From 97d9e61302eafeb9238c13871ae4075265cb629b Mon Sep 17 00:00:00 2001
From: Kermalis <29823718+Kermalis@users.noreply.github.com>
Date: Sat, 18 Nov 2023 20:36:01 -0500
Subject: [PATCH] .NET 8
---
SudokuSolver.sln | 18 +-
SudokuSolver/Cell.cs | 257 ++++++
SudokuSolver/CellSnapshot.cs | 19 +
SudokuSolver/Core/Cell.cs | 152 ----
SudokuSolver/Core/CellSnapshot.cs | 21 -
SudokuSolver/Core/Puzzle.cs | 235 -----
SudokuSolver/Core/Region.cs | 35 -
SudokuSolver/Core/SPoint.cs | 42 -
SudokuSolver/Core/Solver.cs | 74 --
SudokuSolver/Core/Solver_Helpers.cs | 151 ----
SudokuSolver/Core/Solver_Methods.cs | 846 ------------------
SudokuSolver/Core/Solver_Techniques.cs | 41 -
SudokuSolver/Puzzle.cs | 265 ++++++
SudokuSolver/Region.cs | 123 +++
SudokuSolver/SPoint.cs | 56 ++
SudokuSolver/Solver.cs | 268 ++++++
.../Solver_AvoidableRectangle.cs | 120 +++
SudokuSolver/SolverTechniques/Solver_Fish.cs | 82 ++
.../Solver_HiddenRectangle.cs | 75 ++
.../SolverTechniques/Solver_HiddenSingle.cs | 31 +
.../SolverTechniques/Solver_HiddenTuple.cs | 94 ++
.../Solver_LockedCandidate.cs | 44 +
.../SolverTechniques/Solver_NakedTuple.cs | 89 ++
.../SolverTechniques/Solver_PointingTuple.cs | 116 +++
.../Solver_UniqueRectangle.cs | 231 +++++
.../SolverTechniques/Solver_XYChain.cs | 72 ++
.../SolverTechniques/Solver_XYZWing.cs | 58 ++
SudokuSolver/SolverTechniques/Solver_YWing.cs | 63 ++
SudokuSolver/Solver_Techniques.cs | 50 ++
SudokuSolver/SudokuSolver.csproj | 15 +-
SudokuSolver/{Core => }/Utils.cs | 52 +-
SudokuSolverTests/SudokuSolverTests.csproj | 22 +
SudokuSolverTests/TestSolverTechniques.cs | 36 +
SudokuSolverTests/TestUtils.cs | 52 ++
.../MainWindow.Designer.cs | 0
.../UI => SudokuSolverWinForms}/MainWindow.cs | 62 +-
.../MainWindow.resx | 0
.../Program.cs | 5 +-
.../Properties/Icon.ico | Bin
.../Properties/Icon.png | Bin
.../SudokuBoard.cs | 65 +-
.../SudokuSolverWinForms.csproj | 25 +
42 files changed, 2365 insertions(+), 1697 deletions(-)
create mode 100644 SudokuSolver/Cell.cs
create mode 100644 SudokuSolver/CellSnapshot.cs
delete mode 100644 SudokuSolver/Core/Cell.cs
delete mode 100644 SudokuSolver/Core/CellSnapshot.cs
delete mode 100644 SudokuSolver/Core/Puzzle.cs
delete mode 100644 SudokuSolver/Core/Region.cs
delete mode 100644 SudokuSolver/Core/SPoint.cs
delete mode 100644 SudokuSolver/Core/Solver.cs
delete mode 100644 SudokuSolver/Core/Solver_Helpers.cs
delete mode 100644 SudokuSolver/Core/Solver_Methods.cs
delete mode 100644 SudokuSolver/Core/Solver_Techniques.cs
create mode 100644 SudokuSolver/Puzzle.cs
create mode 100644 SudokuSolver/Region.cs
create mode 100644 SudokuSolver/SPoint.cs
create mode 100644 SudokuSolver/Solver.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_AvoidableRectangle.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_Fish.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_HiddenRectangle.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_HiddenSingle.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_HiddenTuple.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_LockedCandidate.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_NakedTuple.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_PointingTuple.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_UniqueRectangle.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_XYChain.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_XYZWing.cs
create mode 100644 SudokuSolver/SolverTechniques/Solver_YWing.cs
create mode 100644 SudokuSolver/Solver_Techniques.cs
rename SudokuSolver/{Core => }/Utils.cs (65%)
create mode 100644 SudokuSolverTests/SudokuSolverTests.csproj
create mode 100644 SudokuSolverTests/TestSolverTechniques.cs
create mode 100644 SudokuSolverTests/TestUtils.cs
rename {SudokuSolver/UI => SudokuSolverWinForms}/MainWindow.Designer.cs (100%)
rename {SudokuSolver/UI => SudokuSolverWinForms}/MainWindow.cs (60%)
rename {SudokuSolver/UI => SudokuSolverWinForms}/MainWindow.resx (100%)
rename {SudokuSolver => SudokuSolverWinForms}/Program.cs (74%)
rename {SudokuSolver => SudokuSolverWinForms}/Properties/Icon.ico (100%)
rename {SudokuSolver => SudokuSolverWinForms}/Properties/Icon.png (100%)
rename {SudokuSolver/UI => SudokuSolverWinForms}/SudokuBoard.cs (71%)
create mode 100644 SudokuSolverWinForms/SudokuSolverWinForms.csproj
diff --git a/SudokuSolver.sln b/SudokuSolver.sln
index a28bbee..d867671 100644
--- a/SudokuSolver.sln
+++ b/SudokuSolver.sln
@@ -1,9 +1,13 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.27428.2002
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33110.190
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SudokuSolver", "SudokuSolver\SudokuSolver.csproj", "{C3556DE7-3124-4995-8BBB-FD9DFF460193}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SudokuSolver", "SudokuSolver\SudokuSolver.csproj", "{C3556DE7-3124-4995-8BBB-FD9DFF460193}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SudokuSolverTests", "SudokuSolverTests\SudokuSolverTests.csproj", "{80009691-BE65-4F91-91DB-DA244CBA5527}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBD}") = "SudokuSolverWinForms", "SudokuSolverWinForms\SudokuSolverWinForms.csproj", "{9B84A03B-038F-4FE9-AFFA-DD64D749C86B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +19,14 @@ Global
{C3556DE7-3124-4995-8BBB-FD9DFF460193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3556DE7-3124-4995-8BBB-FD9DFF460193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3556DE7-3124-4995-8BBB-FD9DFF460193}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80009691-BE65-4F91-91DB-DA244CBA5527}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80009691-BE65-4F91-91DB-DA244CBA5527}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80009691-BE65-4F91-91DB-DA244CBA5527}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80009691-BE65-4F91-91DB-DA244CBA5527}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B84A03B-038F-4FE9-AFFA-DD64D749C86B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B84A03B-038F-4FE9-AFFA-DD64D749C86B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B84A03B-038F-4FE9-AFFA-DD64D749C86B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B84A03B-038F-4FE9-AFFA-DD64D749C86B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/SudokuSolver/Cell.cs b/SudokuSolver/Cell.cs
new file mode 100644
index 0000000..cdf470a
--- /dev/null
+++ b/SudokuSolver/Cell.cs
@@ -0,0 +1,257 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+#if DEBUG
+using System.Diagnostics;
+#endif
+
+namespace Kermalis.SudokuSolver;
+
+#if DEBUG
+[DebuggerDisplay("{DebugString()}", Name = "{ToString()}")]
+#endif
+public sealed class Cell
+{
+ public const int EMPTY_VALUE = 0;
+ /// 6 in Column, 6 in Row, 8 in Block
+ public const int NUM_VISIBLE_CELLS = 6 + 6 + 8;
+
+ internal readonly Puzzle Puzzle;
+ public int OriginalValue { get; private set; }
+ public SPoint Point { get; }
+ public Region Block { get; private set; }
+ public Region Column { get; private set; }
+ public Region Row { get; private set; }
+ /// The cells this cell is grouped with. Block, Row, Column
+ public ReadOnlyCollection VisibleCells { get; private set; }
+
+ public int Value { get; private set; }
+ public HashSet Candidates { get; }
+
+ public List Snapshots { get; }
+
+ internal Cell(Puzzle puzzle, int value, SPoint point)
+ {
+ Puzzle = puzzle;
+
+ OriginalValue = value;
+ Value = value;
+ Point = point;
+
+ Candidates = new HashSet(Utils.OneToNine);
+ Snapshots = [];
+
+ // Will be set in InitRegions
+ Block = null!;
+ Column = null!;
+ Row = null!;
+ // Will be set in InitVisibleCells
+ VisibleCells = null!;
+ }
+ internal void InitRegions()
+ {
+ Block = Puzzle.Blocks[Point.BlockIndex];
+ Column = Puzzle.Columns[Point.Column];
+ Row = Puzzle.Rows[Point.Row];
+ }
+ internal void InitVisibleCells()
+ {
+ int counter = 0;
+ var neighbors = new Cell[NUM_VISIBLE_CELLS];
+ for (int i = 0; i < 9; i++)
+ {
+ // Add 8 neighbors from block
+ Cell other = Block[i];
+ if (other != this)
+ {
+ neighbors[counter++] = other;
+ }
+
+ // Add 6 neighbors from row
+ other = Row[i];
+ if (other.Block != Block)
+ {
+ neighbors[counter++] = other;
+ }
+
+ // Add 6 neighbors from column
+ other = Column[i];
+ if (other.Block != Block)
+ {
+ neighbors[counter++] = other;
+ }
+ }
+ VisibleCells = new ReadOnlyCollection(neighbors);
+ }
+
+ // TODO: Remove
+ public bool ChangeCandidates(int candidate, bool remove = true)
+ {
+ return remove ? Candidates.Remove(candidate) : Candidates.Add(candidate);
+ }
+ internal bool ChangeCandidates(IEnumerable candidates, bool remove = true)
+ {
+ bool changed = false;
+ foreach (int candidate in candidates)
+ {
+ if (remove ? Candidates.Remove(candidate) : Candidates.Add(candidate))
+ {
+ changed = true;
+ }
+ }
+ return changed;
+ }
+ internal static bool ChangeCandidates(IEnumerable cells, int candidate, bool remove = true)
+ {
+ bool changed = false;
+ foreach (Cell cell in cells)
+ {
+ if (remove ? cell.Candidates.Remove(candidate) : cell.Candidates.Add(candidate))
+ {
+ changed = true;
+ }
+ }
+ return changed;
+ }
+ internal static bool ChangeCandidates(IEnumerable cells, IEnumerable candidates, bool remove = true)
+ {
+ bool changed = false;
+ foreach (Cell cell in cells)
+ {
+ foreach (int candidate in candidates)
+ {
+ if (remove ? cell.Candidates.Remove(candidate) : cell.Candidates.Add(candidate))
+ {
+ changed = true;
+ }
+ }
+ }
+ return changed;
+ }
+
+ /// Changes the current value to . is updated.
+ /// If is true, the entire puzzle's candidates are refreshed.
+ internal void Set(int newValue, bool refreshOtherCellCandidates = false)
+ {
+ int oldValue = Value;
+ Value = newValue;
+
+ if (newValue == EMPTY_VALUE)
+ {
+ for (int i = 1; i <= 9; i++)
+ {
+ Candidates.Add(i);
+ }
+ ChangeCandidates(VisibleCells, oldValue, remove: false);
+ }
+ else
+ {
+ Candidates.Clear();
+ ChangeCandidates(VisibleCells, newValue);
+ }
+
+ if (refreshOtherCellCandidates)
+ {
+ Puzzle.RefreshCandidates();
+ }
+ }
+ public void ChangeOriginalValue(int value)
+ {
+ if (value is < EMPTY_VALUE or > 9)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, null);
+ }
+
+ OriginalValue = value;
+ Set(value, refreshOtherCellCandidates: true);
+ }
+ /*internal void CreateSnapshot(bool isCulprit, bool isSemiCulprit)
+ {
+ Span cache = stackalloc int[9];
+ Snapshots.Add(new CellSnapshot(Value, new ReadOnlyCollection(Candidates.AsInt(cache).ToArray()), isCulprit, isSemiCulprit));
+ }*/
+ internal void CreateSnapshot(bool isCulprit, bool isSemiCulprit)
+ {
+ Snapshots.Add(new CellSnapshot(Value, new ReadOnlyCollection(Candidates.ToArray()), isCulprit, isSemiCulprit));
+ }
+
+ public override int GetHashCode()
+ {
+ return Point.GetHashCode();
+ }
+ public override bool Equals(object? obj)
+ {
+ if (obj is Cell other)
+ {
+ return other.Point.Equals(Point);
+ }
+ return false;
+ }
+ public override string ToString()
+ {
+ return Point.ToString();
+ }
+#if DEBUG
+ public string DebugString()
+ {
+ string s = Point.ToString() + " ";
+ if (Value == EMPTY_VALUE)
+ {
+ s += "has candidates: " + Candidates.Print();
+ }
+ else
+ {
+ s += "- " + Value.ToString();
+ }
+ return s;
+ }
+#endif
+
+ /*/// Returns the visible cells that this cell and share.
+ /// Result length is 2 (1 row + 1 column) or 7 (7 row/column) or 13 (7 block + 6 row/column)
+ internal Span IntersectVisibleCells(Cell other, Span cache)
+ {
+ int counter = 0;
+ for (int i = 0; i < NUM_VISIBLE_CELLS; i++)
+ {
+ Cell cell = VisibleCells[i];
+ if (other.VisibleCells.Contains(cell))
+ {
+ cache[counter++] = cell;
+ }
+ }
+ return cache.Slice(0, counter);
+ }
+ /// Returns the visible cells that this cell, , and all share.
+ /// Result length is 0 or 1 (1 row/column) or 6 (6 block/row/column) or 12 (6 block + 6 row/column)
+ internal Span IntersectVisibleCells(Cell otherA, Cell otherB, Span cache)
+ {
+ int counter = 0;
+ for (int i = 0; i < NUM_VISIBLE_CELLS; i++)
+ {
+ Cell cell = VisibleCells[i];
+ if (otherA.VisibleCells.Contains(cell) && otherB.VisibleCells.Contains(cell))
+ {
+ cache[counter++] = cell;
+ }
+ }
+ return cache.Slice(0, counter);
+ }*/
+
+ /*/// Result length is 12 or 14
+ internal Span VisibleCellsExceptRegion(Region except, Span cache)
+ {
+ int counter = 0;
+ for (int i = 0; i < 8 + 6 + 6; i++)
+ {
+ Cell cell = VisibleCells[i];
+ if (except.IndexOf(cell) == -1)
+ {
+ // Add if "except" region does not contain the visible cell
+ cache[counter++] = cell;
+ }
+ }
+ return cache.Slice(0, counter);
+ }*/
+}
diff --git a/SudokuSolver/CellSnapshot.cs b/SudokuSolver/CellSnapshot.cs
new file mode 100644
index 0000000..3038ede
--- /dev/null
+++ b/SudokuSolver/CellSnapshot.cs
@@ -0,0 +1,19 @@
+using System.Collections.ObjectModel;
+
+namespace Kermalis.SudokuSolver;
+
+public sealed class CellSnapshot
+{
+ public int Value { get; }
+ public ReadOnlyCollection Candidates { get; }
+ public bool IsCulprit { get; }
+ public bool IsSemiCulprit { get; }
+
+ internal CellSnapshot(int value, ReadOnlyCollection candidates, bool isCulprit, bool isSemiCulprit)
+ {
+ Value = value;
+ Candidates = candidates;
+ IsCulprit = isCulprit;
+ IsSemiCulprit = isSemiCulprit;
+ }
+}
\ No newline at end of file
diff --git a/SudokuSolver/Core/Cell.cs b/SudokuSolver/Core/Cell.cs
deleted file mode 100644
index 32e9677..0000000
--- a/SudokuSolver/Core/Cell.cs
+++ /dev/null
@@ -1,152 +0,0 @@
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using System.Linq;
-
-namespace Kermalis.SudokuSolver.Core;
-
-[DebuggerDisplay("{DebugString()}", Name = "{ToString()}")]
-internal sealed class Cell
-{
- public const int EMPTY_VALUE = 0;
-
- private readonly Puzzle _puzzle;
- public int OriginalValue { get; private set; }
- public SPoint Point { get; }
- /// All other cells this cell is grouped with. Block, Row, Column
- public ReadOnlyCollection VisibleCells { get; private set; }
-
- public int Value { get; private set; }
- public HashSet Candidates { get; }
-
- public List Snapshots { get; }
-
- public Cell(Puzzle puzzle, int value, SPoint point)
- {
- _puzzle = puzzle;
-
- OriginalValue = value;
- Value = value;
- Point = point;
-
- Candidates = new HashSet(Utils.OneToNine);
- Snapshots = new List();
-
- VisibleCells = null!; // Will be set in CalcVisibleCells
- }
- public void CalcVisibleCells()
- {
- Region block = _puzzle.Blocks[Point.BlockIndex];
- Region col = _puzzle.Columns[Point.X];
- Region row = _puzzle.Rows[Point.Y];
- VisibleCells = new ReadOnlyCollection(block.Union(col).Union(row).Except(new Cell[] { this }).ToArray());
- }
-
- public bool ChangeCandidates(int candidate, bool remove = true)
- {
- return remove ? Candidates.Remove(candidate) : Candidates.Add(candidate);
- }
- public bool ChangeCandidates(IEnumerable candidates, bool remove = true)
- {
- bool changed = false;
- foreach (int value in candidates)
- {
- if (remove ? Candidates.Remove(value) : Candidates.Add(value))
- {
- changed = true;
- }
- }
- return changed;
- }
- public static bool ChangeCandidates(IEnumerable cells, int candidate, bool remove = true)
- {
- bool changed = false;
- foreach (Cell cell in cells)
- {
- if (remove ? cell.Candidates.Remove(candidate) : cell.Candidates.Add(candidate))
- {
- changed = true;
- }
- }
- return changed;
- }
- public static bool ChangeCandidates(IEnumerable cells, IEnumerable candidates, bool remove = true)
- {
- bool changed = false;
- foreach (Cell cell in cells)
- {
- foreach (int value in candidates)
- {
- if (remove ? cell.Candidates.Remove(value) : cell.Candidates.Add(value))
- {
- changed = true;
- }
- }
- }
- return changed;
- }
-
- public void Set(int newValue, bool refreshOtherCellCandidates = false)
- {
- int oldValue = Value;
- Value = newValue;
-
- if (newValue == EMPTY_VALUE)
- {
- for (int i = 1; i <= 9; i++)
- {
- Candidates.Add(i);
- }
- ChangeCandidates(VisibleCells, oldValue, remove: false);
- }
- else
- {
- Candidates.Clear();
- ChangeCandidates(VisibleCells, newValue);
- }
-
- if (refreshOtherCellCandidates)
- {
- _puzzle.RefreshCandidates();
- }
- }
- public void ChangeOriginalValue(int value)
- {
- OriginalValue = value;
- Set(value, refreshOtherCellCandidates: true);
- }
- public void CreateSnapshot(bool isCulprit, bool isSemiCulprit)
- {
- Snapshots.Add(new CellSnapshot(Value, Candidates, isCulprit, isSemiCulprit));
- }
-
- public override int GetHashCode()
- {
- return Point.GetHashCode();
- }
- public override bool Equals(object? obj)
- {
- if (obj is Cell other)
- {
- return other.Point.Equals(Point);
- }
- return false;
- }
- public override string ToString()
- {
- return Point.ToString();
- }
- public string DebugString()
- {
- string s = Point.ToString() + " ";
- if (Value == EMPTY_VALUE)
- {
- s += "has candidates: " + Candidates.Print();
- }
- else
- {
- s += "- " + Value.ToString();
- }
- return s;
- }
-}
diff --git a/SudokuSolver/Core/CellSnapshot.cs b/SudokuSolver/Core/CellSnapshot.cs
deleted file mode 100644
index 4ff31aa..0000000
--- a/SudokuSolver/Core/CellSnapshot.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-
-namespace Kermalis.SudokuSolver.Core;
-
-internal sealed class CellSnapshot
-{
- public int Value { get; }
- public ReadOnlyCollection Candidates { get; }
- public bool IsCulprit { get; }
- public bool IsSemiCulprit { get; }
-
- public CellSnapshot(int value, HashSet candidates, bool isCulprit, bool isSemiCulprit)
- {
- Value = value;
- Candidates = new ReadOnlyCollection(candidates.ToArray());
- IsCulprit = isCulprit;
- IsSemiCulprit = isSemiCulprit;
- }
-}
\ No newline at end of file
diff --git a/SudokuSolver/Core/Puzzle.cs b/SudokuSolver/Core/Puzzle.cs
deleted file mode 100644
index 4c81956..0000000
--- a/SudokuSolver/Core/Puzzle.cs
+++ /dev/null
@@ -1,235 +0,0 @@
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.ComponentModel;
-using System.IO;
-using System.Linq;
-
-namespace Kermalis.SudokuSolver.Core;
-
-internal sealed class Puzzle
-{
- public readonly ReadOnlyCollection Rows;
- public readonly ReadOnlyCollection Columns;
- public readonly ReadOnlyCollection Blocks;
- public readonly ReadOnlyCollection> Regions;
-
- public readonly BindingList Actions;
- public readonly bool IsCustom;
- /// Stored as x,y (col,row)
- private readonly Cell[][] _board;
-
- public Cell this[int x, int y] => _board[x][y];
-
- public Puzzle(int[][] board, bool isCustom)
- {
- IsCustom = isCustom;
- Actions = new BindingList();
-
- _board = new Cell[9][];
- for (int x = 0; x < 9; x++)
- {
- _board[x] = new Cell[9];
- for (int y = 0; y < 9; y++)
- {
- _board[x][y] = new Cell(this, board[x][y], new SPoint(x, y));
- }
- }
-
- var rows = new Region[9];
- var columns = new Region[9];
- var blocks = new Region[9];
- for (int i = 0; i < 9; i++)
- {
- var cells = new Cell[9];
- int c;
-
- for (c = 0; c < 9; c++)
- {
- cells[c] = _board[c][i];
- }
- rows[i] = new Region(cells);
-
- for (c = 0; c < 9; c++)
- {
- cells[c] = _board[i][c];
- }
- columns[i] = new Region(cells);
-
- c = 0;
- int ix = i % 3 * 3;
- int iy = i / 3 * 3;
- for (int x = ix; x < ix + 3; x++)
- {
- for (int y = iy; y < iy + 3; y++)
- {
- cells[c++] = _board[x][y];
- }
- }
- blocks[i] = new Region(cells);
- }
-
- Regions = new ReadOnlyCollection>(new ReadOnlyCollection[3]
- {
- Rows = new ReadOnlyCollection(rows),
- Columns = new ReadOnlyCollection(columns),
- Blocks = new ReadOnlyCollection(blocks)
- });
-
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- _board[x][y].CalcVisibleCells();
- }
- }
- }
-
- public void RefreshCandidates()
- {
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = this[x, y];
- for (int i = 1; i <= 9; i++)
- {
- cell.Candidates.Add(i);
- }
- }
- }
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = this[x, y];
- if (cell.Value != Cell.EMPTY_VALUE)
- {
- cell.Set(cell.Value);
- }
- }
- }
- }
-
- public static Puzzle Load(string fileName)
- {
- string[] fileLines = File.ReadAllLines(fileName);
- if (fileLines.Length != 9)
- {
- throw new InvalidDataException("Puzzle must have 9 rows.");
- }
-
- int[][] board = new int[9][];
- for (int col = 0; col < 9; col++)
- {
- board[col] = new int[9];
- }
-
- for (int i = 0; i < 9; i++)
- {
- string line = fileLines[i];
- if (line.Length != 9)
- {
- throw new InvalidDataException($"Row {i} must have 9 values.");
- }
-
- for (int j = 0; j < 9; j++)
- {
- if (int.TryParse(line[j].ToString(), out int value)) // Anything can represent Cell.EMPTY_VALUE
- {
- board[j][i] = value;
- }
- }
- }
-
- return new Puzzle(board, false);
- }
- public void Save(string fileName)
- {
- using (var file = new StreamWriter(fileName))
- {
- for (int x = 0; x < 9; x++)
- {
- string line = string.Empty;
- for (int y = 0; y < 9; y++)
- {
- Cell cell = this[y, x];
- if (cell.OriginalValue == Cell.EMPTY_VALUE)
- {
- line += '-';
- }
- else
- {
- line += cell.OriginalValue.ToString();
- }
- }
- file.WriteLine(line);
- }
- }
- }
-
- public static string TechniqueFormat(string technique, string format, params object[] args)
- {
- return string.Format(string.Format("{0,-20}", technique) + format, args);
- }
-
- public void LogAction(string action)
- {
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = this[x, y];
- cell.CreateSnapshot(false, false);
- }
- }
- Actions.Add(action);
- }
- public void LogAction(string action, Cell culprit, Cell? semiCulprit)
- {
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = this[x, y];
- cell.CreateSnapshot(culprit == cell, semiCulprit == cell);
- }
- }
- Actions.Add(action);
- }
- public void LogAction(string action, IEnumerable? culprits, Cell? semiCulprit)
- {
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = this[x, y];
- cell.CreateSnapshot(culprits is not null && culprits.Contains(cell), semiCulprit == cell);
- }
- }
- Actions.Add(action);
- }
- public void LogAction(string action, Cell culprit, IEnumerable? semiCulprits)
- {
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = this[x, y];
- cell.CreateSnapshot(culprit == cell, semiCulprits is not null && semiCulprits.Contains(cell));
- }
- }
- Actions.Add(action);
- }
- public void LogAction(string action, IEnumerable? culprits, IEnumerable? semiCulprits)
- {
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = this[x, y];
- cell.CreateSnapshot(culprits is not null && culprits.Contains(cell), semiCulprits is not null && semiCulprits.Contains(cell));
- }
- }
- Actions.Add(action);
- }
-}
diff --git a/SudokuSolver/Core/Region.cs b/SudokuSolver/Core/Region.cs
deleted file mode 100644
index e8771b9..0000000
--- a/SudokuSolver/Core/Region.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Kermalis.SudokuSolver.Core;
-
-internal sealed class Region : IEnumerable
-{
- private readonly Cell[] _cells;
-
- public Cell this[int index] => _cells[index];
-
- public Region(Cell[] cells)
- {
- _cells = (Cell[])cells.Clone();
- }
-
- public IEnumerable GetCellsWithCandidate(int candidate)
- {
- return _cells.Where(c => c.Candidates.Contains(candidate));
- }
- public IEnumerable GetCellsWithCandidates(params int[] candidates)
- {
- return _cells.Where(c => c.Candidates.ContainsAll(candidates));
- }
-
- public IEnumerator GetEnumerator()
- {
- return ((IEnumerable)_cells).GetEnumerator();
- }
- IEnumerator IEnumerable.GetEnumerator()
- {
- return _cells.GetEnumerator();
- }
-}
diff --git a/SudokuSolver/Core/SPoint.cs b/SudokuSolver/Core/SPoint.cs
deleted file mode 100644
index 66ff048..0000000
--- a/SudokuSolver/Core/SPoint.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System;
-
-namespace Kermalis.SudokuSolver.Core;
-
-internal sealed class SPoint
-{
- public int X { get; }
- public int Y { get; }
- public int BlockIndex { get; }
-
- public SPoint(int x, int y)
- {
- X = x;
- Y = y;
- BlockIndex = (x / 3) + (3 * (y / 3));
- }
-
- public override bool Equals(object? obj)
- {
- if (obj is SPoint other)
- {
- return other.X == X && other.Y == Y;
- }
- return false;
- }
- public override int GetHashCode()
- {
- return HashCode.Combine(X, Y);
- }
- public static string RowLetter(int row)
- {
- return ((char)(row + 'A')).ToString();
- }
- public static string ColumnLetter(int column)
- {
- return (column + 1).ToString();
- }
- public override string ToString()
- {
- return RowLetter(Y) + ColumnLetter(X);
- }
-}
diff --git a/SudokuSolver/Core/Solver.cs b/SudokuSolver/Core/Solver.cs
deleted file mode 100644
index 17f75b9..0000000
--- a/SudokuSolver/Core/Solver.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-
-namespace Kermalis.SudokuSolver.Core;
-
-internal sealed partial class Solver
-{
- public Puzzle Puzzle { get; }
-
- public Solver(Puzzle puzzle)
- {
- Puzzle = puzzle;
- }
-
- public void DoWork(object? sender, DoWorkEventArgs e)
- {
- Puzzle.RefreshCandidates();
- Puzzle.LogAction("Begin");
-
- bool solved; // If this is true after a segment, the puzzle is solved and we can break
- do
- {
- solved = true;
- bool changed = CheckForNakedSinglesOrCompletion(ref solved);
-
- // Solved or failed to solve
- if (solved
- || (!changed && !RunTechnique()))
- {
- break;
- }
- } while (true);
-
- e.Result = solved;
- }
- private bool CheckForNakedSinglesOrCompletion(ref bool solved)
- {
- bool changed = false;
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = Puzzle[x, y];
- if (cell.Value == Cell.EMPTY_VALUE)
- {
- solved = false;
- // Check for naked singles
- HashSet a = cell.Candidates;
- if (a.Count == 1)
- {
- int nakedSingle = a.ElementAt(0);
- cell.Set(nakedSingle);
- Puzzle.LogAction(Puzzle.TechniqueFormat("Naked single", "{0}: {1}", cell, nakedSingle), cell, (Cell?)null);
- changed = true;
- }
- }
- }
- }
- return changed;
- }
-
- private bool RunTechnique()
- {
- foreach (SolverTechnique t in _techniques)
- {
- if (t.Function.Invoke(Puzzle))
- {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/SudokuSolver/Core/Solver_Helpers.cs b/SudokuSolver/Core/Solver_Helpers.cs
deleted file mode 100644
index f15d9b6..0000000
--- a/SudokuSolver/Core/Solver_Helpers.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Kermalis.SudokuSolver.Core;
-
-internal sealed partial class Solver
-{
- private static readonly string[] _fishStr = new string[5] { string.Empty, string.Empty, "X-Wing", "Swordfish", "Jellyfish" };
- private static readonly string[] _tupleStr = new string[5] { string.Empty, "single", "pair", "triple", "quadruple" };
- private static readonly string[] _ordinalStr = new string[4] { string.Empty, "1st", "2nd", "3rd" };
-
- // Find X-Wing, Swordfish & Jellyfish
- private static bool FindFish(Puzzle puzzle, int amount)
- {
- for (int candidate = 1; candidate <= 9; candidate++)
- {
- bool DoFish(int loop, int[] indexes)
- {
- if (loop == amount)
- {
- IEnumerable> rowCells = indexes.Select(i => puzzle.Rows[i].GetCellsWithCandidate(candidate)),
- colCells = indexes.Select(i => puzzle.Columns[i].GetCellsWithCandidate(candidate));
-
- IEnumerable rowLengths = rowCells.Select(cells => cells.Count()),
- colLengths = colCells.Select(parr => parr.Count());
-
- if (rowLengths.Max() == amount && rowLengths.Min() > 0 && rowCells.Select(cells => cells.Select(c => c.Point.X)).UniteAll().Count() <= amount)
- {
- IEnumerable row2D = rowCells.UniteAll();
- if (Cell.ChangeCandidates(row2D.Select(c => puzzle.Columns[c.Point.X]).UniteAll().Except(row2D), candidate))
- {
- puzzle.LogAction(Puzzle.TechniqueFormat(_fishStr[amount], "{0}: {1}", row2D.Print(), candidate), row2D, (Cell?)null);
- return true;
- }
- }
- if (colLengths.Max() == amount && colLengths.Min() > 0 && colCells.Select(cells => cells.Select(c => c.Point.Y)).UniteAll().Count() <= amount)
- {
- IEnumerable col2D = colCells.UniteAll();
- if (Cell.ChangeCandidates(col2D.Select(c => puzzle.Rows[c.Point.Y]).UniteAll().Except(col2D), candidate))
- {
- puzzle.LogAction(Puzzle.TechniqueFormat(_fishStr[amount], "{0}: {1}", col2D.Print(), candidate), col2D, (Cell?)null);
- return true;
- }
- }
- }
- else
- {
- for (int i = loop == 0 ? 0 : indexes[loop - 1] + 1; i < 9; i++)
- {
- indexes[loop] = i;
- if (DoFish(loop + 1, indexes))
- {
- return true;
- }
- }
- }
- return false;
- }
-
- if (DoFish(0, new int[amount]))
- {
- return true;
- }
- }
- return false;
- }
-
- // Find hidden pairs/triples/quadruples
- private static bool FindHiddenTuples(Puzzle puzzle, Region region, int amount)
- {
- // If there are only "amount" cells with candidates, we don't have to waste our time
- if (region.Count(c => c.Candidates.Count > 0) == amount)
- {
- return false;
- }
-
- bool DoHiddenTuples(int loop, int[] candidates)
- {
- if (loop == amount)
- {
- IEnumerable cells = candidates.Select(c => region.GetCellsWithCandidate(c)).UniteAll();
- IEnumerable cands = cells.Select(c => c.Candidates).UniteAll();
- if (cells.Count() != amount // There aren't "amount" cells for our tuple to be in
- || cands.Count() == amount // We already know it's a tuple (might be faster to skip this check, idk)
- || !cands.ContainsAll(candidates))
- {
- return false; // If a number in our combo doesn't actually show up in any of our cells
- }
- if (Cell.ChangeCandidates(cells, Utils.OneToNine.Except(candidates)))
- {
- puzzle.LogAction(Puzzle.TechniqueFormat("Hidden " + _tupleStr[amount], "{0}: {1}", cells.Print(), candidates.Print()), cells, (Cell?)null);
- return true;
- }
- }
- else
- {
- for (int i = candidates[loop == 0 ? loop : loop - 1] + 1; i <= 9; i++)
- {
- candidates[loop] = i;
- if (DoHiddenTuples(loop + 1, candidates))
- {
- return true;
- }
- }
- }
- return false;
- }
-
- return DoHiddenTuples(0, new int[amount]);
- }
-
- // Find naked pairs/triples/quadruples
- private static bool FindNakedTuples(Puzzle puzzle, Region region, int amount)
- {
- bool DoNakedTuples(int loop, Cell[] cells, int[] indexes)
- {
- if (loop == amount)
- {
- IEnumerable combo = cells.Select(c => c.Candidates).UniteAll();
- if (combo.Count() == amount)
- {
- if (Cell.ChangeCandidates(indexes.Select(i => region[i].VisibleCells).IntersectAll(), combo))
- {
- puzzle.LogAction(Puzzle.TechniqueFormat("Naked " + _tupleStr[amount], "{0}: {1}", cells.Print(), combo.Print()), cells, (Cell?)null);
- return true;
- }
- }
- }
- else
- {
- for (int i = loop == 0 ? 0 : indexes[loop - 1] + 1; i < 9; i++)
- {
- Cell c = region[i];
- if (c.Candidates.Count == 0)
- {
- continue;
- }
- cells[loop] = c;
- indexes[loop] = i;
- if (DoNakedTuples(loop + 1, cells, indexes))
- {
- return true;
- }
- }
- }
- return false;
- }
-
- return DoNakedTuples(0, new Cell[amount], new int[amount]);
- }
-}
diff --git a/SudokuSolver/Core/Solver_Methods.cs b/SudokuSolver/Core/Solver_Methods.cs
deleted file mode 100644
index 286d782..0000000
--- a/SudokuSolver/Core/Solver_Methods.cs
+++ /dev/null
@@ -1,846 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-
-namespace Kermalis.SudokuSolver.Core;
-
-internal sealed partial class Solver
-{
- private static bool AvoidableRectangle(Puzzle puzzle)
- {
- for (int type = 1; type <= 2; type++)
- {
- for (int x1 = 0; x1 < 9; x1++)
- {
- Region c1 = puzzle.Columns[x1];
- for (int x2 = x1 + 1; x2 < 9; x2++)
- {
- Region c2 = puzzle.Columns[x2];
- for (int y1 = 0; y1 < 9; y1++)
- {
- for (int y2 = y1 + 1; y2 < 9; y2++)
- {
- for (int value1 = 1; value1 <= 9; value1++)
- {
- for (int value2 = value1 + 1; value2 <= 9; value2++)
- {
- int[] candidates = new int[] { value1, value2 };
- var cells = new Cell[] { c1[y1], c1[y2], c2[y1], c2[y2] };
- if (cells.Any(c => c.OriginalValue != Cell.EMPTY_VALUE))
- {
- continue;
- }
-
- IEnumerable alreadySet = cells.Where(c => c.Value != Cell.EMPTY_VALUE),
- notSet = cells.Where(c => c.Value == Cell.EMPTY_VALUE);
-
- switch (type)
- {
- case 1:
- {
- if (alreadySet.Count() != 3)
- {
- continue;
- }
- break;
- }
- case 2:
- {
- if (alreadySet.Count() != 2)
- {
- continue;
- }
- break;
- }
- }
- var pairs = new Cell[][]
- {
- new Cell[] { cells[0], cells[3] },
- new Cell[] { cells[1], cells[2] }
- };
- foreach (Cell[] pair in pairs)
- {
- Cell[] otherPair = pair == pairs[0] ? pairs[1] : pairs[0];
- foreach (int i in candidates)
- {
- int otherVal = candidates.Single(ca => ca != i);
- if (((pair[0].Value == i && pair[1].Value == Cell.EMPTY_VALUE && pair[1].Candidates.Count == 2 && pair[1].Candidates.Contains(i))
- || (pair[1].Value == i && pair[0].Value == Cell.EMPTY_VALUE && pair[0].Candidates.Count == 2 && pair[0].Candidates.Contains(i)))
- && otherPair.All(c => c.Value == otherVal || (c.Candidates.Count == 2 && c.Candidates.Contains(otherVal))))
- {
- goto breakpairs;
- }
- }
- }
- continue; // Did not find
- breakpairs:
- bool changed = false;
- switch (type)
- {
- case 1:
- {
- Cell cell = notSet.ElementAt(0);
- if (cell.Candidates.Count == 2)
- {
- cell.Set(cell.Candidates.Except(candidates).ElementAt(0));
- }
- else
- {
- cell.ChangeCandidates(cell.Candidates.Intersect(candidates));
- }
- changed = true;
- break;
- }
- case 2:
- {
- IEnumerable commonCandidates = notSet.Select(c => c.Candidates.Except(candidates)).IntersectAll();
- if (commonCandidates.Any()
- && Cell.ChangeCandidates(notSet.Select(c => c.VisibleCells).IntersectAll(), commonCandidates))
- {
- changed = true;
- }
- break;
- }
- }
-
- if (changed)
- {
- puzzle.LogAction(Puzzle.TechniqueFormat("Avoidable rectangle", "{0}: {1}", cells.Print(), candidates.Print()), cells, (Cell?)null);
- return true;
- }
- }
- }
- }
- }
- }
- }
- }
- return false;
- }
-
- private static bool HiddenRectangle(Puzzle puzzle)
- {
- for (int x1 = 0; x1 < 9; x1++)
- {
- Region c1 = puzzle.Columns[x1];
- for (int x2 = x1 + 1; x2 < 9; x2++)
- {
- Region c2 = puzzle.Columns[x2];
- for (int y1 = 0; y1 < 9; y1++)
- {
- for (int y2 = y1 + 1; y2 < 9; y2++)
- {
- for (int value1 = 1; value1 <= 9; value1++)
- {
- for (int value2 = value1 + 1; value2 <= 9; value2++)
- {
- int[] candidates = new int[] { value1, value2 };
- var cells = new Cell[] { c1[y1], c1[y2], c2[y1], c2[y2] };
- if (cells.Any(c => !c.Candidates.ContainsAll(candidates)))
- {
- continue;
- }
- ILookup l = cells.ToLookup(c => c.Candidates.Count);
- IEnumerable gtTwo = l.Where(g => g.Key > 2).SelectMany(g => g);
- int gtTwoCount = gtTwo.Count();
- if (gtTwoCount < 2 || gtTwoCount > 3)
- {
- continue;
- }
-
- bool changed = false;
- foreach (Cell c in l[2])
- {
- int eks = c.Point.X == x1 ? x2 : x1,
- why = c.Point.Y == y1 ? y2 : y1;
- foreach (int i in candidates)
- {
- if (!puzzle.Rows[why].GetCellsWithCandidate(i).Except(cells).Any() // "i" only appears in our UR
- && !puzzle.Columns[eks].GetCellsWithCandidate(i).Except(cells).Any())
- {
- Cell diag = puzzle[eks, why];
- if (diag.Candidates.Count == 2)
- {
- diag.Set(i);
- }
- else
- {
- diag.ChangeCandidates(i == value1 ? value2 : value1);
- }
- changed = true;
- }
- }
- }
- if (changed)
- {
- puzzle.LogAction(Puzzle.TechniqueFormat("Hidden rectangle", "{0}: {1}", cells.Print(), candidates.Print()), cells, (Cell?)null);
- return true;
- }
- }
- }
- }
- }
- }
- }
- return false;
- }
-
- private static bool UniqueRectangle(Puzzle puzzle)
- {
- for (int type = 1; type <= 6; type++) // Type
- {
- for (int x1 = 0; x1 < 9; x1++)
- {
- Region c1 = puzzle.Columns[x1];
- for (int x2 = x1 + 1; x2 < 9; x2++)
- {
- Region c2 = puzzle.Columns[x2];
- for (int y1 = 0; y1 < 9; y1++)
- {
- for (int y2 = y1 + 1; y2 < 9; y2++)
- {
- for (int value1 = 1; value1 <= 9; value1++)
- {
- for (int value2 = value1 + 1; value2 <= 9; value2++)
- {
- int[] candidates = new int[] { value1, value2 };
- var cells = new Cell[] { c1[y1], c1[y2], c2[y1], c2[y2] };
- if (cells.Any(c => !c.Candidates.ContainsAll(candidates)))
- {
- continue;
- }
-
- ILookup l = cells.ToLookup(c => c.Candidates.Count);
- Cell[] gtTwo = l.Where(g => g.Key > 2).SelectMany(g => g).ToArray(),
- two = l[2].ToArray(), three = l[3].ToArray(), four = l[4].ToArray();
-
- switch (type) // Check for candidate counts
- {
- case 1:
- {
- if (two.Length != 3 || gtTwo.Length != 1)
- {
- continue;
- }
- break;
- }
- case 2:
- case 6:
- {
- if (two.Length != 2 || three.Length != 2)
- {
- continue;
- }
- break;
- }
- case 3:
- {
- if (two.Length != 2 || gtTwo.Length != 2)
- {
- continue;
- }
- break;
- }
- case 4:
- {
- if (two.Length != 2 || three.Length != 1 || four.Length != 1)
- {
- continue;
- }
- break;
- }
- case 5:
- {
- if (two.Length != 1 || three.Length != 3)
- {
- continue;
- }
- break;
- }
- }
-
- switch (type) // Check for extra rules
- {
- case 1:
- {
- if (gtTwo[0].Candidates.Count == 3)
- {
- gtTwo[0].Set(gtTwo[0].Candidates.Single(c => !candidates.Contains(c)));
- }
- else
- {
- Cell.ChangeCandidates(gtTwo, candidates);
- }
- break;
- }
- case 2:
- {
- if (!three[0].Candidates.SetEquals(three[1].Candidates))
- {
- continue;
- }
- if (!Cell.ChangeCandidates(three[0].VisibleCells.Intersect(three[1].VisibleCells), three[0].Candidates.Except(candidates)))
- {
- continue;
- }
- break;
- }
- case 3:
- {
- if (gtTwo[0].Point.X != gtTwo[1].Point.X && gtTwo[0].Point.Y != gtTwo[1].Point.Y)
- {
- continue; // Must be non-diagonal
- }
- IEnumerable others = gtTwo[0].Candidates.Except(candidates).Union(gtTwo[1].Candidates.Except(candidates));
- if (others.Count() > 4 || others.Count() < 2)
- {
- continue;
- }
- IEnumerable nSubset = ((gtTwo[0].Point.Y == gtTwo[1].Point.Y) ? // Same row
- puzzle.Rows[gtTwo[0].Point.Y] : puzzle.Columns[gtTwo[0].Point.X])
- .Where(c => c.Candidates.ContainsAny(others) && !c.Candidates.ContainsAny(Utils.OneToNine.Except(others)));
- if (nSubset.Count() != others.Count() - 1)
- {
- continue;
- }
- if (!Cell.ChangeCandidates(nSubset.Union(gtTwo).Select(c => c.VisibleCells).IntersectAll(), others))
- {
- continue;
- }
- break;
- }
- case 4:
- {
- int[] remove = new int[1];
- if (four[0].Point.BlockIndex == three[0].Point.BlockIndex)
- {
- if (puzzle.Blocks[four[0].Point.BlockIndex].GetCellsWithCandidate(value1).Count() == 2)
- {
- remove[0] = value2;
- }
- else if (puzzle.Blocks[four[0].Point.BlockIndex].GetCellsWithCandidate(value2).Count() == 2)
- {
- remove[0] = value1;
- }
- }
- if (remove[0] != 0) // They share the same row/column but not the same block
- {
- if (three[0].Point.X == three[0].Point.X)
- {
- if (puzzle.Columns[four[0].Point.X].GetCellsWithCandidate(value1).Count() == 2)
- {
- remove[0] = value2;
- }
- else if (puzzle.Columns[four[0].Point.X].GetCellsWithCandidate(value2).Count() == 2)
- {
- remove[0] = value1;
- }
- }
- else
- {
- if (puzzle.Rows[four[0].Point.Y].GetCellsWithCandidate(value1).Count() == 2)
- {
- remove[0] = value2;
- }
- else if (puzzle.Rows[four[0].Point.Y].GetCellsWithCandidate(value2).Count() == 2)
- {
- remove[0] = value1;
- }
- }
- }
- else
- {
- continue;
- }
- Cell.ChangeCandidates(cells.Except(l[2]), remove);
- break;
- }
- case 5:
- {
- if (!three[0].Candidates.SetEquals(three[1].Candidates) || !three[1].Candidates.SetEquals(three[2].Candidates))
- {
- continue;
- }
- if (!Cell.ChangeCandidates(three.Select(c => c.VisibleCells).IntersectAll(), three[0].Candidates.Except(candidates)))
- {
- continue;
- }
- break;
- }
- case 6:
- {
- if (three[0].Point.X == three[1].Point.X)
- {
- continue;
- }
- int set;
- if (c1.GetCellsWithCandidate(value1).Count() == 2 && c2.GetCellsWithCandidate(value1).Count() == 2 // Check if "v" only appears in the UR
- && puzzle.Rows[two[0].Point.Y].GetCellsWithCandidate(value1).Count() == 2
- && puzzle.Rows[two[1].Point.Y].GetCellsWithCandidate(value1).Count() == 2)
- {
- set = value1;
- }
- else if (c1.GetCellsWithCandidate(value2).Count() == 2 && c2.GetCellsWithCandidate(value2).Count() == 2
- && puzzle.Rows[two[0].Point.Y].GetCellsWithCandidate(value2).Count() == 2
- && puzzle.Rows[two[1].Point.Y].GetCellsWithCandidate(value2).Count() == 2)
- {
- set = value2;
- }
- else
- {
- continue;
- }
- two[0].Set(set);
- two[1].Set(set);
- break;
- }
- }
-
- puzzle.LogAction(Puzzle.TechniqueFormat("Unique rectangle", "{0}: {1}", cells.Print(), candidates.Print()), cells, (Cell?)null);
- return true;
- }
- }
- }
- }
- }
- }
- }
- return false;
- }
-
- private static bool XYChain(Puzzle puzzle)
- {
- bool Recursion(Cell startCell, List ignore, Cell currentCell, int theOneThatWillEndItAllBaybee, int mustFind)
- {
- ignore.Add(currentCell);
- IEnumerable visible = currentCell.VisibleCells.Except(ignore);
- foreach (Cell cell in visible)
- {
- if (cell.Candidates.Count != 2)
- {
- continue; // Must have two candidates
- }
- if (!cell.Candidates.Contains(mustFind))
- {
- continue; // Must have "mustFind"
- }
-
- int otherCandidate = cell.Candidates.Except(new int[] { mustFind }).Single();
- // Check end condition
- if (otherCandidate == theOneThatWillEndItAllBaybee && startCell != currentCell)
- {
- Cell[] commonVisibleWithStartCell = cell.VisibleCells.Intersect(startCell.VisibleCells).ToArray();
- if (commonVisibleWithStartCell.Length > 0)
- {
- IEnumerable commonWithEndingCandidate = commonVisibleWithStartCell.Where(c => c.Candidates.Contains(theOneThatWillEndItAllBaybee));
- if (Cell.ChangeCandidates(commonWithEndingCandidate, theOneThatWillEndItAllBaybee))
- {
- ignore.Remove(startCell); // Remove here because we're now using "ignore" as "semiCulprits" and exiting
- var culprits = new Cell[] { startCell, cell };
- puzzle.LogAction(Puzzle.TechniqueFormat("XY-Chain", "{0}-{1}: {2}", culprits.Print(), ignore.SingleOrMultiToString(), theOneThatWillEndItAllBaybee), culprits, ignore);
- return true;
- }
- }
- }
- // Loop again
- if (Recursion(startCell, ignore, cell, theOneThatWillEndItAllBaybee, otherCandidate))
- {
- return true;
- }
- }
- ignore.Remove(currentCell);
- return false;
- }
-
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- Cell cell = puzzle[x, y];
- if (cell.Candidates.Count != 2)
- {
- continue; // Must have two candidates
- }
- var ignore = new List();
- int start1 = cell.Candidates.ElementAt(0);
- int start2 = cell.Candidates.ElementAt(1);
- if (Recursion(cell, ignore, cell, start1, start2) || Recursion(cell, ignore, cell, start2, start1))
- {
- return true;
- }
- }
- }
- return false;
- }
-
- private static bool XYZWing(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- bool FindXYZWings(Region region)
- {
- bool changed = false;
- Cell[] cells2 = region.Where(c => c.Candidates.Count == 2).ToArray();
- Cell[] cells3 = region.Where(c => c.Candidates.Count == 3).ToArray();
- if (cells2.Length > 0 && cells3.Length > 0)
- {
- for (int j = 0; j < cells2.Length; j++)
- {
- Cell c2 = cells2[j];
- for (int k = 0; k < cells3.Length; k++)
- {
- Cell c3 = cells3[k];
- if (c2.Candidates.Intersect(c3.Candidates).Count() != 2)
- {
- continue;
- }
-
- IEnumerable c3Sees = c3.VisibleCells.Except(region)
- .Where(c => c.Candidates.Count == 2 // If it has 2 candidates
- && c.Candidates.Intersect(c3.Candidates).Count() == 2 // Shares them both with p3
- && c.Candidates.Intersect(c2.Candidates).Count() == 1); // And shares one with p2
- foreach (Cell c2_2 in c3Sees)
- {
- IEnumerable allSee = c2.VisibleCells.Intersect(c3.VisibleCells).Intersect(c2_2.VisibleCells);
- int allHave = c2.Candidates.Intersect(c3.Candidates).Intersect(c2_2.Candidates).Single(); // Will be 1 Length
- if (Cell.ChangeCandidates(allSee, allHave))
- {
- var culprits = new Cell[] { c2, c3, c2_2 };
- puzzle.LogAction(Puzzle.TechniqueFormat("XYZ-Wing", "{0}: {1}", culprits.Print(), allHave), culprits, (Cell?)null);
- changed = true;
- }
- }
- }
- }
- }
- return changed;
- }
-
- if (FindXYZWings(puzzle.Rows[i]) || FindXYZWings(puzzle.Columns[i]))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool YWing(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- bool FindYWings(Region region)
- {
- Cell[] cells = region.Where(c => c.Candidates.Count == 2).ToArray();
- if (cells.Length > 1)
- {
- for (int j = 0; j < cells.Length; j++)
- {
- Cell c1 = cells[j];
- for (int k = j + 1; k < cells.Length; k++)
- {
- Cell c2 = cells[k];
- IEnumerable inter = c1.Candidates.Intersect(c2.Candidates);
- if (inter.Count() != 1)
- {
- continue;
- }
-
- int other1 = c1.Candidates.Except(inter).ElementAt(0),
- other2 = c2.Candidates.Except(inter).ElementAt(0);
-
- var a = new Cell[] { c1, c2 };
- foreach (Cell cell in a)
- {
- IEnumerable c3a = cell.VisibleCells.Except(cells).Where(c => c.Candidates.Count == 2 && c.Candidates.Intersect(new int[] { other1, other2 }).Count() == 2);
- if (c3a.Count() == 1) // Example: p1 and p3 see each other, so remove similarities from p2 and p3
- {
- Cell c3 = c3a.ElementAt(0);
- Cell cOther = a.Single(c => c != cell);
- IEnumerable commonCells = cOther.VisibleCells.Intersect(c3.VisibleCells);
- int candidate = cOther.Candidates.Intersect(c3.Candidates).Single(); // Will just be 1 candidate
- if (Cell.ChangeCandidates(commonCells, candidate))
- {
- var culprits = new Cell[] { c1, c2, c3 };
- puzzle.LogAction(Puzzle.TechniqueFormat("Y-Wing", "{0}: {1}", culprits.Print(), candidate), culprits, (Cell?)null);
- return true;
- }
- }
- }
- }
- }
- }
- return false;
- }
-
- if (FindYWings(puzzle.Rows[i]) || FindYWings(puzzle.Columns[i]))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool Jellyfish(Puzzle puzzle)
- {
- return FindFish(puzzle, 4);
- }
-
- private static bool Swordfish(Puzzle puzzle)
- {
- return FindFish(puzzle, 3);
- }
-
- private static bool XWing(Puzzle puzzle)
- {
- return FindFish(puzzle, 2);
- }
-
- private static bool PointingTuple(Puzzle puzzle)
- {
- for (int i = 0; i < 3; i++)
- {
- var blockrow = new Cell[3][];
- var blockcol = new Cell[3][];
- for (int r = 0; r < 3; r++)
- {
- blockrow[r] = puzzle.Blocks[r + (i * 3)].ToArray();
- blockcol[r] = puzzle.Blocks[i + (r * 3)].ToArray();
- }
-
- for (int r = 0; r < 3; r++) // 3 blocks in a blockrow/blockcolumn
- {
- int[][] rowCandidates = new int[3][];
- int[][] colCand = new int[3][];
- for (int j = 0; j < 3; j++) // 3 rows/columns in block
- {
- // The 3 cells' candidates in a block's row/column
- rowCandidates[j] = blockrow[r].GetRowInBlock(j).Select(c => c.Candidates).UniteAll().ToArray();
- colCand[j] = blockcol[r].GetColumnInBlock(j).Select(c => c.Candidates).UniteAll().ToArray();
- }
-
- bool RemovePointingTuple(bool doRows, int rcIndex, IEnumerable candidates)
- {
- bool changed = false;
- for (int j = 0; j < 3; j++)
- {
- if (j == r)
- {
- continue;
- }
-
- Cell[] rcs = doRows ? blockrow[j].GetRowInBlock(rcIndex) : blockcol[j].GetColumnInBlock(rcIndex);
- if (Cell.ChangeCandidates(rcs, candidates))
- {
- changed = true;
- }
- }
-
- if (changed)
- {
- Cell[] culprits = doRows ? blockrow[r].GetRowInBlock(rcIndex) : blockcol[r].GetColumnInBlock(rcIndex);
- puzzle.LogAction(Puzzle.TechniqueFormat("Pointing tuple",
- "Starting in block{0} {1}'s {2} block, {3} {0}: {4}",
- doRows ? "row" : "column", i + 1, _ordinalStr[r + 1], _ordinalStr[rcIndex + 1], candidates.SingleOrMultiToString()), culprits, (Cell?)null);
- }
- return changed;
- }
-
- // Now check if a row has a distinct candidate
- IEnumerable zero_distinct = rowCandidates[0].Except(rowCandidates[1]).Except(rowCandidates[2]);
- if (zero_distinct.Any())
- {
- if (RemovePointingTuple(true, 0, zero_distinct))
- {
- return true;
- }
- }
- IEnumerable one_distinct = rowCandidates[1].Except(rowCandidates[0]).Except(rowCandidates[2]);
- if (one_distinct.Any())
- {
- if (RemovePointingTuple(true, 1, one_distinct))
- {
- return true;
- }
- }
- IEnumerable two_distinct = rowCandidates[2].Except(rowCandidates[0]).Except(rowCandidates[1]);
- if (two_distinct.Any())
- {
- if (RemovePointingTuple(true, 2, two_distinct))
- {
- return true;
- }
- }
-
- // Now check if a column has a distinct candidate
- zero_distinct = colCand[0].Except(colCand[1]).Except(colCand[2]);
- if (zero_distinct.Any())
- {
- if (RemovePointingTuple(false, 0, zero_distinct))
- {
- return true;
- }
- }
- one_distinct = colCand[1].Except(colCand[0]).Except(colCand[2]);
- if (one_distinct.Any())
- {
- if (RemovePointingTuple(false, 1, one_distinct))
- {
- return true;
- }
- }
- two_distinct = colCand[2].Except(colCand[0]).Except(colCand[1]);
- if (two_distinct.Any())
- {
- if (RemovePointingTuple(false, 2, two_distinct))
- {
- return true;
- }
- }
- }
- }
- return false;
- }
-
- private static bool LockedCandidate(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- for (int candidate = 1; candidate <= 9; candidate++)
- {
- bool FindLockedCandidates(bool doRows)
- {
- IEnumerable cellsWithCandidates = (doRows ? puzzle.Rows : puzzle.Columns)[i].GetCellsWithCandidate(candidate);
-
- // Even if a block only has these candidates for this "k" value, it'd be slower to check that before cancelling "BlacklistCandidates"
- if (cellsWithCandidates.Count() == 3 || cellsWithCandidates.Count() == 2)
- {
- int[] blocks = cellsWithCandidates.Select(c => c.Point.BlockIndex).Distinct().ToArray();
- if (blocks.Length == 1)
- {
- if (Cell.ChangeCandidates(puzzle.Blocks[blocks[0]].Except(cellsWithCandidates), candidate))
- {
- puzzle.LogAction(Puzzle.TechniqueFormat("Locked candidate",
- "{4} {0} locks within block {1}: {2}: {3}",
- doRows ? SPoint.RowLetter(i) : SPoint.ColumnLetter(i), blocks[0] + 1, cellsWithCandidates.Print(), candidate, doRows ? "Row" : "Column"), cellsWithCandidates, (Cell?)null);
- return true;
- }
- }
- }
- return false;
- }
- if (FindLockedCandidates(true) || FindLockedCandidates(false))
- {
- return true;
- }
- }
- }
- return false;
- }
-
- private static bool HiddenQuadruple(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- if (FindHiddenTuples(puzzle, puzzle.Blocks[i], 4)
- || FindHiddenTuples(puzzle, puzzle.Rows[i], 4)
- || FindHiddenTuples(puzzle, puzzle.Columns[i], 4))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool NakedQuadruple(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- if (FindNakedTuples(puzzle, puzzle.Blocks[i], 4)
- || FindNakedTuples(puzzle, puzzle.Rows[i], 4)
- || FindNakedTuples(puzzle, puzzle.Columns[i], 4))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool HiddenTriple(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- if (FindHiddenTuples(puzzle, puzzle.Blocks[i], 3)
- || FindHiddenTuples(puzzle, puzzle.Rows[i], 3)
- || FindHiddenTuples(puzzle, puzzle.Columns[i], 3))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool NakedTriple(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- if (FindNakedTuples(puzzle, puzzle.Blocks[i], 3)
- || FindNakedTuples(puzzle, puzzle.Rows[i], 3)
- || FindNakedTuples(puzzle, puzzle.Columns[i], 3))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool HiddenPair(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- if (FindHiddenTuples(puzzle, puzzle.Blocks[i], 2)
- || FindHiddenTuples(puzzle, puzzle.Rows[i], 2)
- || FindHiddenTuples(puzzle, puzzle.Columns[i], 2))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool NakedPair(Puzzle puzzle)
- {
- for (int i = 0; i < 9; i++)
- {
- if (FindNakedTuples(puzzle, puzzle.Blocks[i], 2)
- || FindNakedTuples(puzzle, puzzle.Rows[i], 2)
- || FindNakedTuples(puzzle, puzzle.Columns[i], 2))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool HiddenSingle(Puzzle puzzle)
- {
- bool changed = false;
- for (int i = 0; i < 9; i++)
- {
- foreach (ReadOnlyCollection region in puzzle.Regions)
- {
- for (int candidate = 1; candidate <= 9; candidate++)
- {
- Cell[] c = region[i].GetCellsWithCandidate(candidate).ToArray();
- if (c.Length == 1)
- {
- c[0].Set(candidate);
- puzzle.LogAction(Puzzle.TechniqueFormat("Hidden single", "{0}: {1}", c[0], candidate), c[0], (Cell?)null);
- changed = true;
- }
- }
- }
- }
- return changed;
- }
-}
diff --git a/SudokuSolver/Core/Solver_Techniques.cs b/SudokuSolver/Core/Solver_Techniques.cs
deleted file mode 100644
index 2df943a..0000000
--- a/SudokuSolver/Core/Solver_Techniques.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System;
-
-namespace Kermalis.SudokuSolver.Core;
-
-internal sealed partial class Solver
-{
- private sealed class SolverTechnique
- {
- public Func Function { get; }
- /// Currently unused.
- public string Url { get; }
-
- public SolverTechnique(Func function, string url)
- {
- Function = function;
- Url = url;
- }
- }
-
- private static readonly SolverTechnique[] _techniques = new[]
- {
- new SolverTechnique(HiddenSingle, "Hidden single"),
- new SolverTechnique(NakedPair, "https://hodoku.sourceforge.net/en/tech_naked.php#n2"),
- new SolverTechnique(HiddenPair, "https://hodoku.sourceforge.net/en/tech_hidden.php#h2"),
- new SolverTechnique(LockedCandidate, "https://hodoku.sourceforge.net/en/tech_intersections.php#lc1"),
- new SolverTechnique(PointingTuple, "https://hodoku.sourceforge.net/en/tech_intersections.php#lc1"),
- new SolverTechnique(NakedTriple, "https://hodoku.sourceforge.net/en/tech_naked.php#n3"),
- new SolverTechnique(HiddenTriple, "https://hodoku.sourceforge.net/en/tech_hidden.php#h3"),
- new SolverTechnique(XWing, "https://hodoku.sourceforge.net/en/tech_fishb.php#bf2"),
- new SolverTechnique(Swordfish, "https://hodoku.sourceforge.net/en/tech_fishb.php#bf3"),
- new SolverTechnique(YWing, "https://www.sudokuwiki.org/Y_Wing_Strategy"),
- new SolverTechnique(XYZWing, "https://www.sudokuwiki.org/XYZ_Wing"),
- new SolverTechnique(XYChain, "https://www.sudokuwiki.org/XY_Chains"),
- new SolverTechnique(NakedQuadruple, "https://hodoku.sourceforge.net/en/tech_naked.php#n4"),
- new SolverTechnique(HiddenQuadruple, "https://hodoku.sourceforge.net/en/tech_hidden.php#h4"),
- new SolverTechnique(Jellyfish, "https://hodoku.sourceforge.net/en/tech_fishb.php#bf4"),
- new SolverTechnique(UniqueRectangle, "https://hodoku.sourceforge.net/en/tech_ur.php"),
- new SolverTechnique(HiddenRectangle, "https://hodoku.sourceforge.net/en/tech_ur.php#hr"),
- new SolverTechnique(AvoidableRectangle, "https://hodoku.sourceforge.net/en/tech_ur.php#ar"),
- };
-}
diff --git a/SudokuSolver/Puzzle.cs b/SudokuSolver/Puzzle.cs
new file mode 100644
index 0000000..3e207ff
--- /dev/null
+++ b/SudokuSolver/Puzzle.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Text;
+
+namespace Kermalis.SudokuSolver;
+
+public sealed class Puzzle
+{
+ public ReadOnlyCollection Rows { get; }
+ public ReadOnlyCollection Columns { get; }
+ public ReadOnlyCollection Blocks { get; }
+ public ReadOnlyCollection> Regions { get; }
+
+ public bool IsCustom { get; }
+ /// Stored as x,y (col,row)
+ private readonly Cell[][] _board;
+
+ public Cell this[int col, int row] => _board[col][row];
+
+ private Puzzle(int[][] board, bool isCustom)
+ {
+ IsCustom = isCustom;
+
+ _board = new Cell[9][];
+ for (int col = 0; col < 9; col++)
+ {
+ _board[col] = new Cell[9];
+ for (int row = 0; row < 9; row++)
+ {
+ _board[col][row] = new Cell(this, board[col][row], new SPoint(col, row));
+ }
+ }
+
+ var rows = new Region[9];
+ var columns = new Region[9];
+ var blocks = new Region[9];
+ var cells = new Cell[9];
+ for (int i = 0; i < 9; i++)
+ {
+ int j;
+ for (j = 0; j < 9; j++)
+ {
+ cells[j] = _board[j][i];
+ }
+ rows[i] = new Region(cells);
+
+ for (j = 0; j < 9; j++)
+ {
+ cells[j] = _board[i][j];
+ }
+ columns[i] = new Region(cells);
+
+ j = 0;
+ int x = i % 3 * 3;
+ int y = i / 3 * 3;
+ for (int col = x; col < x + 3; col++)
+ {
+ for (int row = y; row < y + 3; row++)
+ {
+ cells[j++] = _board[col][row];
+ }
+ }
+ blocks[i] = new Region(cells);
+ }
+
+ Regions = new ReadOnlyCollection>(new ReadOnlyCollection[3]
+ {
+ Rows = new ReadOnlyCollection(rows),
+ Columns = new ReadOnlyCollection(columns),
+ Blocks = new ReadOnlyCollection(blocks)
+ });
+
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ _board[col][row].InitRegions();
+ }
+ }
+
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ _board[col][row].InitVisibleCells();
+ }
+ }
+ }
+
+ internal void RefreshCandidates()
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = _board[col][row];
+ for (int i = 1; i <= 9; i++)
+ {
+ cell.Candidates.Add(i);
+ }
+ }
+ }
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = _board[col][row];
+ if (cell.Value != Cell.EMPTY_VALUE)
+ {
+ cell.Set(cell.Value);
+ }
+ }
+ }
+ }
+
+ public static Puzzle CreateCustom()
+ {
+ int[][] board = new int[9][];
+ for (int col = 0; col < 9; col++)
+ {
+ board[col] = new int[9];
+ }
+ return new Puzzle(board, true);
+ }
+ public static Puzzle Parse(ReadOnlySpan inRows)
+ {
+ if (inRows.Length != 9)
+ {
+ throw new InvalidDataException("Puzzle must have 9 rows.");
+ }
+
+ int[][] board = new int[9][];
+ for (int col = 0; col < 9; col++)
+ {
+ board[col] = new int[9];
+ }
+
+ for (int row = 0; row < 9; row++)
+ {
+ string line = inRows[row];
+ if (line.Length != 9)
+ {
+ throw new InvalidDataException($"Row {row} must have 9 values.");
+ }
+
+ for (int col = 0; col < 9; col++)
+ {
+ if (int.TryParse(line[col].ToString(), out int value) && value is >= 1 and <= 9)
+ {
+ board[col][row] = value;
+ }
+ else
+ {
+ board[col][row] = Cell.EMPTY_VALUE; // Anything else can represent Cell.EMPTY_VALUE
+ }
+ }
+ }
+
+ return new Puzzle(board, false);
+ }
+
+ public void Reset()
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = _board[col][row];
+ if (cell.Value != cell.OriginalValue)
+ {
+ cell.Set(Cell.EMPTY_VALUE);
+ }
+ }
+ }
+ }
+ /// Returns true if any digit is repeated. Can be called even if the puzzle isn't solved yet.
+ public bool CheckForErrors()
+ {
+ for (int val = 1; val <= 9; val++)
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (Blocks[i].CheckForDuplicateValue(val)
+ || Rows[i].CheckForDuplicateValue(val)
+ || Columns[i].CheckForDuplicateValue(val))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ for (int row = 0; row < 9; row++)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ Cell cell = _board[col][row];
+ if (cell.OriginalValue == Cell.EMPTY_VALUE)
+ {
+ sb.Append('-');
+ }
+ else
+ {
+ sb.Append(cell.OriginalValue);
+ }
+ }
+ if (row != 8)
+ {
+ sb.AppendLine();
+ }
+ }
+ return sb.ToString();
+ }
+ public string ToStringFancy()
+ {
+ var sb = new StringBuilder();
+ for (int row = 0; row < 9; row++)
+ {
+ if (row % 3 == 0)
+ {
+ for (int col = 0; col < 13; col++)
+ {
+ sb.Append('—');
+ }
+ sb.AppendLine();
+ }
+ for (int col = 0; col < 9; col++)
+ {
+ if (col % 3 == 0)
+ {
+ sb.Append('┃');
+ }
+
+ Cell cell = _board[col][row];
+ if (cell.Value == Cell.EMPTY_VALUE)
+ {
+ sb.Append(' ');
+ }
+ else
+ {
+ sb.Append(cell.Value);
+ }
+
+ if (col == 8)
+ {
+ sb.Append('┃');
+ }
+ }
+ sb.AppendLine();
+ if (row == 8)
+ {
+ for (int col = 0; col < 13; col++)
+ {
+ sb.Append('—');
+ }
+ }
+ }
+ return sb.ToString();
+ }
+}
diff --git a/SudokuSolver/Region.cs b/SudokuSolver/Region.cs
new file mode 100644
index 0000000..5245eb8
--- /dev/null
+++ b/SudokuSolver/Region.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+public sealed class Region : IEnumerable
+{
+ private readonly Cell[] _cells;
+
+ public Cell this[int index] => _cells[index];
+
+ internal Region(ReadOnlySpan cells)
+ {
+ _cells = cells.ToArray();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return ((IEnumerable)_cells).GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _cells.GetEnumerator();
+ }
+
+ public int IndexOf(Cell cell)
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (_cells[i] == cell)
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /*/// Result length is [0,9]
+ internal Span GetCellsWithCandidate(int candidate, Span cache)
+ {
+ int counter = 0;
+ for (int i = 0; i < 9; i++)
+ {
+ Cell cell = _cells[i];
+ if (cell.Candidates[candidate])
+ {
+ cache[counter++] = cell;
+ }
+ }
+ return cache.Slice(0, counter);
+ }*/
+ public IEnumerable GetCellsWithCandidate(int candidate)
+ {
+ return _cells.Where(c => c.Candidates.Contains(candidate));
+ }
+ public IEnumerable GetCellsWithCandidates(params int[] candidates)
+ {
+ return _cells.Where(c => c.Candidates.ContainsAll(candidates));
+ }
+
+ /*/// Result length is [0,9]
+ internal Span GetCellsWithCandidateCount(int numCandidates, Span cache)
+ {
+ int counter = 0;
+ for (int i = 0; i < 9; i++)
+ {
+ Cell cell = _cells[i];
+ if (cell.Candidates.Count == numCandidates)
+ {
+ cache[counter++] = cell;
+ }
+ }
+ return cache.Slice(0, counter);
+ }
+ internal int CountCellsWithCandidates()
+ {
+ int counter = 0;
+ for (int i = 0; i < 9; i++)
+ {
+ Cell cell = _cells[i];
+ if (cell.Candidates.Count != 0)
+ {
+ counter++;
+ }
+ }
+ return counter;
+ }*/
+
+ /*/// Returns all cells except for the ones in .
+ /// Result length is [0,9]
+ internal Span Except(ReadOnlySpan other, Span cache)
+ {
+ int retLength = 0;
+ for (int i = 0; i < 9; i++)
+ {
+ Cell c = _cells[i];
+ if (other.SimpleIndexOf(c) == -1)
+ {
+ cache[retLength++] = c;
+ }
+ }
+ return cache.Slice(0, retLength);
+ }*/
+
+ internal bool CheckForDuplicateValue(int val)
+ {
+ bool foundValueAlready = false;
+ for (int i = 0; i < 9; i++)
+ {
+ if (_cells[i].Value == val)
+ {
+ if (foundValueAlready)
+ {
+ return true;
+ }
+ foundValueAlready = true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/SPoint.cs b/SudokuSolver/SPoint.cs
new file mode 100644
index 0000000..0d9202f
--- /dev/null
+++ b/SudokuSolver/SPoint.cs
@@ -0,0 +1,56 @@
+using System;
+
+namespace Kermalis.SudokuSolver;
+
+public readonly struct SPoint
+{
+ public int Column { get; }
+ public int Row { get; }
+ public int BlockIndex { get; }
+
+ internal SPoint(int col, int row)
+ {
+ Column = col;
+ Row = row;
+ BlockIndex = (col / 3) + (3 * (row / 3));
+ }
+
+ public bool Equals(int col, int row)
+ {
+ return Column == col && Row == row;
+ }
+ public static string RowLetter(int row)
+ {
+ return ((char)(row + 'A')).ToString();
+ }
+ public static string ColumnLetter(int col)
+ {
+ return (col + 1).ToString();
+ }
+
+ public static bool operator ==(SPoint left, SPoint right)
+ {
+ return left.Equals(right);
+ }
+ public static bool operator !=(SPoint left, SPoint right)
+ {
+ return !(left == right);
+ }
+ public override bool Equals(object? obj)
+ {
+ if (obj is SPoint other)
+ {
+ return other.Column == Column && other.Row == Row;
+ }
+ return false;
+ }
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Column, Row);
+ }
+ public override string ToString()
+ {
+ return RowLetter(Row) + ColumnLetter(Column);
+ }
+
+}
diff --git a/SudokuSolver/Solver.cs b/SudokuSolver/Solver.cs
new file mode 100644
index 0000000..7d999fa
--- /dev/null
+++ b/SudokuSolver/Solver.cs
@@ -0,0 +1,268 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+
+namespace Kermalis.SudokuSolver;
+
+public sealed partial class Solver
+{
+ public Puzzle Puzzle { get; }
+ public BindingList Actions { get; }
+
+ public Solver(Puzzle puzzle)
+ {
+ Puzzle = puzzle;
+ Actions = new BindingList();
+
+ _techniques = InitSolverTechniques();
+ }
+ public static Solver CreateCustomPuzzle()
+ {
+ var s = new Solver(Puzzle.CreateCustom());
+ s.LogAction("Custom puzzle created");
+ return s;
+ }
+
+ public void SetOriginalCellValue(Cell cell, int value)
+ {
+ if (cell.Puzzle != Puzzle)
+ {
+ throw new ArgumentOutOfRangeException(nameof(cell), cell, "Cell belongs to another puzzle");
+ }
+
+ cell.ChangeOriginalValue(value);
+ string action = TechniqueFormat("Changed cell", cell.ToString());
+ LogAction(action, cell);
+ }
+
+ public bool TrySolve()
+ {
+ Puzzle.RefreshCandidates();
+ LogAction("Begin");
+
+ do
+ {
+ if (CheckForNakedSinglesOrCompletion(out bool changed))
+ {
+ LogAction("Solver completed the puzzle");
+ return true;
+ }
+ if (changed)
+ {
+ continue;
+ }
+ if (!RunTechnique())
+ {
+ LogAction("Solver failed the puzzle");
+ return false;
+ }
+ } while (true);
+ }
+ public bool TrySolveAsync(CancellationToken ct)
+ {
+ Puzzle.RefreshCandidates();
+ LogAction("Begin");
+
+ do
+ {
+ if (CheckForNakedSinglesOrCompletion(out bool changed))
+ {
+ LogAction("Solver completed the puzzle");
+ return true;
+ }
+ if (ct.IsCancellationRequested)
+ {
+ break;
+ }
+ if (changed)
+ {
+ continue;
+ }
+ if (!RunTechnique())
+ {
+ LogAction("Solver failed the puzzle");
+ return false;
+ }
+ if (ct.IsCancellationRequested)
+ {
+ break;
+ }
+ } while (true);
+
+ LogAction("Solver cancelled");
+ throw new OperationCanceledException(ct);
+ }
+ private bool CheckForNakedSinglesOrCompletion(out bool changed)
+ {
+ changed = false;
+ bool solved = true;
+ again:
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ if (cell.Value != Cell.EMPTY_VALUE)
+ {
+ continue;
+ }
+
+ // Empty cell... check for naked single
+ solved = false;
+ /*if (cell.Candidates.TryGetCount1(out int nakedSingle))
+ {
+ cell.Set(nakedSingle);
+
+ string action = TechniqueFormat("Naked single", "{0}: {1}",
+ cell, nakedSingle);
+ LogAction(action, cell);
+
+ changed = true;
+ goto again; // Restart the search for naked singles since we have the potential to create new ones
+ }*/
+ HashSet a = cell.Candidates;
+ if (a.Count == 1)
+ {
+ int nakedSingle = a.ElementAt(0);
+ cell.Set(nakedSingle);
+
+ string action = TechniqueFormat("Naked single", "{0}: {1}",
+ cell, nakedSingle);
+ LogAction(action, cell);
+
+ changed = true;
+ goto again; // Restart the search for naked singles since we have the potential to create new ones
+ }
+ }
+ }
+ return solved;
+ }
+
+ private bool RunTechnique()
+ {
+ foreach (SolverTechnique t in _techniques)
+ {
+ if (t.Function.Invoke())
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static string TechniqueFormat(string technique, string format, params object[] args)
+ {
+ return string.Format(string.Format("{0,-20}", technique) + format, args);
+ }
+ private void LogAction(string action)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(false, false);
+ }
+ }
+ Actions.Add(action);
+ }
+ private void LogAction(string action, Cell culprit)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(culprit == cell, false);
+ }
+ }
+ Actions.Add(action);
+ }
+ private void LogAction(string action, Cell culprit, Cell semiCulprit)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(culprit == cell, semiCulprit == cell);
+ }
+ }
+ Actions.Add(action);
+ }
+ /*// TODO: Remove .AsSpan() and (Cell?)null from callers
+ private void LogAction(string action, ReadOnlySpan culprits)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(culprits.SimpleIndexOf(cell) != -1, false);
+ }
+ }
+ Actions.Add(action);
+ }
+ private void LogAction(string action, ReadOnlySpan culprits, Cell semiCulprit)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(culprits.SimpleIndexOf(cell) != -1, semiCulprit == cell);
+ }
+ }
+ Actions.Add(action);
+ }
+ private void LogAction(string action, Cell culprit, ReadOnlySpan semiCulprits)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(culprit == cell, semiCulprits.SimpleIndexOf(cell) != -1);
+ }
+ }
+ Actions.Add(action);
+ }
+ private void LogAction(string action, ReadOnlySpan culprits, ReadOnlySpan semiCulprits)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(culprits.SimpleIndexOf(cell) != -1, semiCulprits.SimpleIndexOf(cell) != -1);
+ }
+ }
+ Actions.Add(action);
+ }*/
+ public void LogAction(string action, IEnumerable culprits)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(culprits.Contains(cell), false);
+ }
+ }
+ Actions.Add(action);
+ }
+ public void LogAction(string action, IEnumerable culprits, IEnumerable semiCulprits)
+ {
+ for (int col = 0; col < 9; col++)
+ {
+ for (int row = 0; row < 9; row++)
+ {
+ Cell cell = Puzzle[col, row];
+ cell.CreateSnapshot(culprits.Contains(cell), semiCulprits.Contains(cell));
+ }
+ }
+ Actions.Add(action);
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_AvoidableRectangle.cs b/SudokuSolver/SolverTechniques/Solver_AvoidableRectangle.cs
new file mode 100644
index 0000000..8bb08a8
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_AvoidableRectangle.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool AvoidableRectangle()
+ {
+ for (int type = 1; type <= 2; type++)
+ {
+ for (int x1 = 0; x1 < 9; x1++)
+ {
+ Region c1 = Puzzle.Columns[x1];
+ for (int x2 = x1 + 1; x2 < 9; x2++)
+ {
+ Region c2 = Puzzle.Columns[x2];
+ for (int y1 = 0; y1 < 9; y1++)
+ {
+ for (int y2 = y1 + 1; y2 < 9; y2++)
+ {
+ for (int value1 = 1; value1 <= 9; value1++)
+ {
+ for (int value2 = value1 + 1; value2 <= 9; value2++)
+ {
+ int[] candidates = [value1, value2];
+ var cells = new Cell[] { c1[y1], c1[y2], c2[y1], c2[y2] };
+ if (cells.Any(c => c.OriginalValue != Cell.EMPTY_VALUE))
+ {
+ continue;
+ }
+
+ IEnumerable alreadySet = cells.Where(c => c.Value != Cell.EMPTY_VALUE),
+ notSet = cells.Where(c => c.Value == Cell.EMPTY_VALUE);
+
+ switch (type)
+ {
+ case 1:
+ {
+ if (alreadySet.Count() != 3)
+ {
+ continue;
+ }
+ break;
+ }
+ case 2:
+ {
+ if (alreadySet.Count() != 2)
+ {
+ continue;
+ }
+ break;
+ }
+ }
+ var pairs = new Cell[][]
+ {
+ new Cell[] { cells[0], cells[3] },
+ new Cell[] { cells[1], cells[2] }
+ };
+ foreach (Cell[] pair in pairs)
+ {
+ Cell[] otherPair = pair == pairs[0] ? pairs[1] : pairs[0];
+ foreach (int i in candidates)
+ {
+ int otherVal = candidates.Single(ca => ca != i);
+ if (((pair[0].Value == i && pair[1].Value == Cell.EMPTY_VALUE && pair[1].Candidates.Count == 2 && pair[1].Candidates.Contains(i))
+ || (pair[1].Value == i && pair[0].Value == Cell.EMPTY_VALUE && pair[0].Candidates.Count == 2 && pair[0].Candidates.Contains(i)))
+ && otherPair.All(c => c.Value == otherVal || (c.Candidates.Count == 2 && c.Candidates.Contains(otherVal))))
+ {
+ goto breakpairs;
+ }
+ }
+ }
+ continue; // Did not find
+ breakpairs:
+ bool changed = false;
+ switch (type)
+ {
+ case 1:
+ {
+ Cell cell = notSet.ElementAt(0);
+ if (cell.Candidates.Count == 2)
+ {
+ cell.Set(cell.Candidates.Except(candidates).ElementAt(0));
+ }
+ else
+ {
+ cell.ChangeCandidates(cell.Candidates.Intersect(candidates));
+ }
+ changed = true;
+ break;
+ }
+ case 2:
+ {
+ IEnumerable commonCandidates = notSet.Select(c => c.Candidates.Except(candidates)).IntersectAll();
+ if (commonCandidates.Any()
+ && Cell.ChangeCandidates(notSet.Select(c => c.VisibleCells).IntersectAll(), commonCandidates))
+ {
+ changed = true;
+ }
+ break;
+ }
+ }
+
+ if (changed)
+ {
+ LogAction(TechniqueFormat("Avoidable rectangle", "{0}: {1}", cells.Print(), candidates.Print()), cells);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_Fish.cs b/SudokuSolver/SolverTechniques/Solver_Fish.cs
new file mode 100644
index 0000000..d081dde
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_Fish.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private static ReadOnlySpan FishStr => new string[5] { string.Empty, string.Empty, "X-Wing", "Swordfish", "Jellyfish" };
+
+ private bool Jellyfish()
+ {
+ return Fish_Find(4);
+ }
+
+ private bool Swordfish()
+ {
+ return Fish_Find(3);
+ }
+
+ private bool XWing()
+ {
+ return Fish_Find(2);
+ }
+
+
+ // Find X-Wing, Swordfish & Jellyfish
+ private bool Fish_Find(int amount)
+ {
+ for (int candidate = 1; candidate <= 9; candidate++)
+ {
+ bool DoFish(int loop, int[] indexes)
+ {
+ if (loop == amount)
+ {
+ IEnumerable> rowCells = indexes.Select(i => Puzzle.Rows[i].GetCellsWithCandidate(candidate));
+ IEnumerable> colCells = indexes.Select(i => Puzzle.Columns[i].GetCellsWithCandidate(candidate));
+
+ IEnumerable rowLengths = rowCells.Select(cells => cells.Count());
+ IEnumerable colLengths = colCells.Select(parr => parr.Count());
+
+ if (rowLengths.Max() == amount && rowLengths.Min() > 0 && rowCells.Select(cells => cells.Select(c => c.Point.Column)).UniteAll().Count() <= amount)
+ {
+ IEnumerable row2D = rowCells.UniteAll();
+ if (Cell.ChangeCandidates(row2D.Select(c => Puzzle.Columns[c.Point.Column]).UniteAll().Except(row2D), candidate))
+ {
+ LogAction(TechniqueFormat(FishStr[amount], "{0}: {1}", row2D.Print(), candidate), row2D);
+ return true;
+ }
+ }
+ if (colLengths.Max() == amount && colLengths.Min() > 0 && colCells.Select(cells => cells.Select(c => c.Point.Row)).UniteAll().Count() <= amount)
+ {
+ IEnumerable col2D = colCells.UniteAll();
+ if (Cell.ChangeCandidates(col2D.Select(c => Puzzle.Rows[c.Point.Row]).UniteAll().Except(col2D), candidate))
+ {
+ LogAction(TechniqueFormat(FishStr[amount], "{0}: {1}", col2D.Print(), candidate), col2D);
+ return true;
+ }
+ }
+ }
+ else
+ {
+ for (int i = loop == 0 ? 0 : indexes[loop - 1] + 1; i < 9; i++)
+ {
+ indexes[loop] = i;
+ if (DoFish(loop + 1, indexes))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ if (DoFish(0, new int[amount]))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_HiddenRectangle.cs b/SudokuSolver/SolverTechniques/Solver_HiddenRectangle.cs
new file mode 100644
index 0000000..88bc65a
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_HiddenRectangle.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool HiddenRectangle()
+ {
+ for (int x1 = 0; x1 < 9; x1++)
+ {
+ Region c1 = Puzzle.Columns[x1];
+ for (int x2 = x1 + 1; x2 < 9; x2++)
+ {
+ Region c2 = Puzzle.Columns[x2];
+ for (int y1 = 0; y1 < 9; y1++)
+ {
+ for (int y2 = y1 + 1; y2 < 9; y2++)
+ {
+ for (int value1 = 1; value1 <= 9; value1++)
+ {
+ for (int value2 = value1 + 1; value2 <= 9; value2++)
+ {
+ int[] candidates = [value1, value2];
+ var cells = new Cell[] { c1[y1], c1[y2], c2[y1], c2[y2] };
+ if (cells.Any(c => !c.Candidates.ContainsAll(candidates)))
+ {
+ continue;
+ }
+ ILookup l = cells.ToLookup(c => c.Candidates.Count);
+ IEnumerable gtTwo = l.Where(g => g.Key > 2).SelectMany(g => g);
+ int gtTwoCount = gtTwo.Count();
+ if (gtTwoCount < 2 || gtTwoCount > 3)
+ {
+ continue;
+ }
+
+ bool changed = false;
+ foreach (Cell c in l[2])
+ {
+ int eks = c.Point.Column == x1 ? x2 : x1,
+ why = c.Point.Row == y1 ? y2 : y1;
+ foreach (int i in candidates)
+ {
+ if (!Puzzle.Rows[why].GetCellsWithCandidate(i).Except(cells).Any() // "i" only appears in our UR
+ && !Puzzle.Columns[eks].GetCellsWithCandidate(i).Except(cells).Any())
+ {
+ Cell diag = Puzzle[eks, why];
+ if (diag.Candidates.Count == 2)
+ {
+ diag.Set(i);
+ }
+ else
+ {
+ diag.ChangeCandidates(i == value1 ? value2 : value1);
+ }
+ changed = true;
+ }
+ }
+ }
+ if (changed)
+ {
+ LogAction(TechniqueFormat("Hidden rectangle", "{0}: {1}", cells.Print(), candidates.Print()), cells);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_HiddenSingle.cs b/SudokuSolver/SolverTechniques/Solver_HiddenSingle.cs
new file mode 100644
index 0000000..d11c931
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_HiddenSingle.cs
@@ -0,0 +1,31 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool HiddenSingle()
+ {
+ bool changed = false;
+
+ for (int i = 0; i < 9; i++)
+ {
+ foreach (ReadOnlyCollection region in Puzzle.Regions)
+ {
+ for (int candidate = 1; candidate <= 9; candidate++)
+ {
+ Cell[] c = region[i].GetCellsWithCandidate(candidate).ToArray();
+ if (c.Length == 1)
+ {
+ c[0].Set(candidate);
+ LogAction(TechniqueFormat("Hidden single", "{0}: {1}", c[0], candidate), c[0]);
+ changed = true;
+ }
+ }
+ }
+ }
+
+ return changed;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_HiddenTuple.cs b/SudokuSolver/SolverTechniques/Solver_HiddenTuple.cs
new file mode 100644
index 0000000..5e9403b
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_HiddenTuple.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool HiddenQuadruple()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (HiddenTuple_Find(Puzzle.Blocks[i], 4)
+ || HiddenTuple_Find(Puzzle.Rows[i], 4)
+ || HiddenTuple_Find(Puzzle.Columns[i], 4))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool HiddenTriple()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (HiddenTuple_Find(Puzzle.Blocks[i], 3)
+ || HiddenTuple_Find(Puzzle.Rows[i], 3)
+ || HiddenTuple_Find(Puzzle.Columns[i], 3))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool HiddenPair()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (HiddenTuple_Find(Puzzle.Blocks[i], 2)
+ || HiddenTuple_Find(Puzzle.Rows[i], 2)
+ || HiddenTuple_Find(Puzzle.Columns[i], 2))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Find hidden pairs/triples/quadruples
+ private bool HiddenTuple_Find(Region region, int amount)
+ {
+ // If there are only "amount" cells with candidates, we don't have to waste our time
+ if (region.Count(c => c.Candidates.Count > 0) == amount)
+ {
+ return false;
+ }
+
+ bool DoHiddenTuples(int loop, int[] candidates)
+ {
+ if (loop == amount)
+ {
+ IEnumerable cells = candidates.Select(c => region.GetCellsWithCandidate(c)).UniteAll();
+ IEnumerable cands = cells.Select(c => c.Candidates).UniteAll();
+ if (cells.Count() != amount // There aren't "amount" cells for our tuple to be in
+ || cands.Count() == amount // We already know it's a tuple (might be faster to skip this check, idk)
+ || !cands.ContainsAll(candidates))
+ {
+ return false; // If a number in our combo doesn't actually show up in any of our cells
+ }
+ if (Cell.ChangeCandidates(cells, Utils.OneToNine.Except(candidates)))
+ {
+ LogAction(TechniqueFormat("Hidden " + TupleStr[amount], "{0}: {1}", cells.Print(), candidates.Print()), cells);
+ return true;
+ }
+ }
+ else
+ {
+ for (int i = candidates[loop == 0 ? loop : loop - 1] + 1; i <= 9; i++)
+ {
+ candidates[loop] = i;
+ if (DoHiddenTuples(loop + 1, candidates))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ return DoHiddenTuples(0, new int[amount]);
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_LockedCandidate.cs b/SudokuSolver/SolverTechniques/Solver_LockedCandidate.cs
new file mode 100644
index 0000000..51fe3a2
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_LockedCandidate.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool LockedCandidate()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ for (int candidate = 1; candidate <= 9; candidate++)
+ {
+ bool FindLockedCandidates(bool doRows)
+ {
+ IEnumerable cellsWithCandidates = (doRows ? Puzzle.Rows : Puzzle.Columns)[i].GetCellsWithCandidate(candidate);
+
+ // Even if a block only has these candidates for this "k" value, it'd be slower to check that before cancelling "BlacklistCandidates"
+ if (cellsWithCandidates.Count() is 2 or 3)
+ {
+ int[] blocks = cellsWithCandidates.Select(c => c.Point.BlockIndex).Distinct().ToArray();
+ if (blocks.Length == 1)
+ {
+ if (Cell.ChangeCandidates(Puzzle.Blocks[blocks[0]].Except(cellsWithCandidates), candidate))
+ {
+ LogAction(TechniqueFormat("Locked candidate",
+ "{4} {0} locks within block {1}: {2}: {3}",
+ doRows ? SPoint.RowLetter(i) : SPoint.ColumnLetter(i), blocks[0] + 1, cellsWithCandidates.Print(), candidate, doRows ? "Row" : "Column"), cellsWithCandidates);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ if (FindLockedCandidates(true) || FindLockedCandidates(false))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_NakedTuple.cs b/SudokuSolver/SolverTechniques/Solver_NakedTuple.cs
new file mode 100644
index 0000000..7d6f9a9
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_NakedTuple.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool NakedQuadruple()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (NakedTuple_Find(Puzzle.Blocks[i], 4)
+ || NakedTuple_Find(Puzzle.Rows[i], 4)
+ || NakedTuple_Find(Puzzle.Columns[i], 4))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool NakedTriple()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (NakedTuple_Find(Puzzle.Blocks[i], 3)
+ || NakedTuple_Find(Puzzle.Rows[i], 3)
+ || NakedTuple_Find(Puzzle.Columns[i], 3))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool NakedPair()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (NakedTuple_Find(Puzzle.Blocks[i], 2)
+ || NakedTuple_Find(Puzzle.Rows[i], 2)
+ || NakedTuple_Find(Puzzle.Columns[i], 2))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool NakedTuple_Find(Region region, int amount)
+ {
+ bool DoNakedTuples(int loop, Cell[] cells, int[] indexes)
+ {
+ if (loop == amount)
+ {
+ IEnumerable combo = cells.Select(c => c.Candidates).UniteAll();
+ if (combo.Count() == amount)
+ {
+ if (Cell.ChangeCandidates(indexes.Select(i => region[i].VisibleCells).IntersectAll(), combo))
+ {
+ LogAction(TechniqueFormat("Naked " + TupleStr[amount], "{0}: {1}", cells.Print(), combo.Print()), cells);
+ return true;
+ }
+ }
+ }
+ else
+ {
+ for (int i = loop == 0 ? 0 : indexes[loop - 1] + 1; i < 9; i++)
+ {
+ Cell c = region[i];
+ if (c.Candidates.Count == 0)
+ {
+ continue;
+ }
+ cells[loop] = c;
+ indexes[loop] = i;
+ if (DoNakedTuples(loop + 1, cells, indexes))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ return DoNakedTuples(0, new Cell[amount], new int[amount]);
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_PointingTuple.cs b/SudokuSolver/SolverTechniques/Solver_PointingTuple.cs
new file mode 100644
index 0000000..1d20729
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_PointingTuple.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private static ReadOnlySpan OrdinalStr => new string[4] { string.Empty, "1st", "2nd", "3rd" };
+
+ private bool PointingTuple()
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ var blockrow = new Cell[3][];
+ var blockcol = new Cell[3][];
+ for (int r = 0; r < 3; r++)
+ {
+ blockrow[r] = [.. Puzzle.Blocks[r + (i * 3)]];
+ blockcol[r] = [.. Puzzle.Blocks[i + (r * 3)]];
+ }
+
+ for (int r = 0; r < 3; r++) // 3 blocks in a blockrow/blockcolumn
+ {
+ int[][] rowCandidates = new int[3][];
+ int[][] colCand = new int[3][];
+ for (int j = 0; j < 3; j++) // 3 rows/columns in block
+ {
+ // The 3 cells' candidates in a block's row/column
+ rowCandidates[j] = blockrow[r].GetRowInBlock(j).Select(c => c.Candidates).UniteAll().ToArray();
+ colCand[j] = blockcol[r].GetColumnInBlock(j).Select(c => c.Candidates).UniteAll().ToArray();
+ }
+
+ bool RemovePointingTuple(bool doRows, int rcIndex, IEnumerable candidates)
+ {
+ bool changed = false;
+ for (int j = 0; j < 3; j++)
+ {
+ if (j == r)
+ {
+ continue;
+ }
+
+ Cell[] rcs = doRows ? blockrow[j].GetRowInBlock(rcIndex) : blockcol[j].GetColumnInBlock(rcIndex);
+ if (Cell.ChangeCandidates(rcs, candidates))
+ {
+ changed = true;
+ }
+ }
+
+ if (changed)
+ {
+ Cell[] culprits = doRows ? blockrow[r].GetRowInBlock(rcIndex) : blockcol[r].GetColumnInBlock(rcIndex);
+ LogAction(TechniqueFormat("Pointing tuple",
+ "Starting in block{0} {1}'s {2} block, {3} {0}: {4}",
+ doRows ? "row" : "column", i + 1, OrdinalStr[r + 1], OrdinalStr[rcIndex + 1], candidates.SingleOrMultiToString()), culprits);
+ }
+ return changed;
+ }
+
+ // Now check if a row has a distinct candidate
+ IEnumerable zero_distinct = rowCandidates[0].Except(rowCandidates[1]).Except(rowCandidates[2]);
+ if (zero_distinct.Any())
+ {
+ if (RemovePointingTuple(true, 0, zero_distinct))
+ {
+ return true;
+ }
+ }
+ IEnumerable one_distinct = rowCandidates[1].Except(rowCandidates[0]).Except(rowCandidates[2]);
+ if (one_distinct.Any())
+ {
+ if (RemovePointingTuple(true, 1, one_distinct))
+ {
+ return true;
+ }
+ }
+ IEnumerable two_distinct = rowCandidates[2].Except(rowCandidates[0]).Except(rowCandidates[1]);
+ if (two_distinct.Any())
+ {
+ if (RemovePointingTuple(true, 2, two_distinct))
+ {
+ return true;
+ }
+ }
+
+ // Now check if a column has a distinct candidate
+ zero_distinct = colCand[0].Except(colCand[1]).Except(colCand[2]);
+ if (zero_distinct.Any())
+ {
+ if (RemovePointingTuple(false, 0, zero_distinct))
+ {
+ return true;
+ }
+ }
+ one_distinct = colCand[1].Except(colCand[0]).Except(colCand[2]);
+ if (one_distinct.Any())
+ {
+ if (RemovePointingTuple(false, 1, one_distinct))
+ {
+ return true;
+ }
+ }
+ two_distinct = colCand[2].Except(colCand[0]).Except(colCand[1]);
+ if (two_distinct.Any())
+ {
+ if (RemovePointingTuple(false, 2, two_distinct))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_UniqueRectangle.cs b/SudokuSolver/SolverTechniques/Solver_UniqueRectangle.cs
new file mode 100644
index 0000000..e3868b0
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_UniqueRectangle.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool UniqueRectangle()
+ {
+ for (int type = 1; type <= 6; type++) // Type
+ {
+ for (int x1 = 0; x1 < 9; x1++)
+ {
+ Region c1 = Puzzle.Columns[x1];
+ for (int x2 = x1 + 1; x2 < 9; x2++)
+ {
+ Region c2 = Puzzle.Columns[x2];
+ for (int y1 = 0; y1 < 9; y1++)
+ {
+ for (int y2 = y1 + 1; y2 < 9; y2++)
+ {
+ for (int value1 = 1; value1 <= 9; value1++)
+ {
+ for (int value2 = value1 + 1; value2 <= 9; value2++)
+ {
+ int[] candidates = [value1, value2];
+ var cells = new Cell[] { c1[y1], c1[y2], c2[y1], c2[y2] };
+ if (cells.Any(c => !c.Candidates.ContainsAll(candidates)))
+ {
+ continue;
+ }
+
+ ILookup l = cells.ToLookup(c => c.Candidates.Count);
+ Cell[] gtTwo = l.Where(g => g.Key > 2).SelectMany(g => g).ToArray(),
+ two = l[2].ToArray(), three = l[3].ToArray(), four = l[4].ToArray();
+
+ switch (type) // Check for candidate counts
+ {
+ case 1:
+ {
+ if (two.Length != 3 || gtTwo.Length != 1)
+ {
+ continue;
+ }
+ break;
+ }
+ case 2:
+ case 6:
+ {
+ if (two.Length != 2 || three.Length != 2)
+ {
+ continue;
+ }
+ break;
+ }
+ case 3:
+ {
+ if (two.Length != 2 || gtTwo.Length != 2)
+ {
+ continue;
+ }
+ break;
+ }
+ case 4:
+ {
+ if (two.Length != 2 || three.Length != 1 || four.Length != 1)
+ {
+ continue;
+ }
+ break;
+ }
+ case 5:
+ {
+ if (two.Length != 1 || three.Length != 3)
+ {
+ continue;
+ }
+ break;
+ }
+ }
+
+ switch (type) // Check for extra rules
+ {
+ case 1:
+ {
+ if (gtTwo[0].Candidates.Count == 3)
+ {
+ gtTwo[0].Set(gtTwo[0].Candidates.Single(c => !candidates.Contains(c)));
+ }
+ else
+ {
+ Cell.ChangeCandidates(gtTwo, candidates);
+ }
+ break;
+ }
+ case 2:
+ {
+ if (!three[0].Candidates.SetEquals(three[1].Candidates))
+ {
+ continue;
+ }
+ if (!Cell.ChangeCandidates(three[0].VisibleCells.Intersect(three[1].VisibleCells), three[0].Candidates.Except(candidates)))
+ {
+ continue;
+ }
+ break;
+ }
+ case 3:
+ {
+ if (gtTwo[0].Point.Column != gtTwo[1].Point.Column && gtTwo[0].Point.Row != gtTwo[1].Point.Row)
+ {
+ continue; // Must be non-diagonal
+ }
+ IEnumerable others = gtTwo[0].Candidates.Except(candidates).Union(gtTwo[1].Candidates.Except(candidates));
+ if (others.Count() > 4 || others.Count() < 2)
+ {
+ continue;
+ }
+ IEnumerable nSubset = ((gtTwo[0].Point.Row == gtTwo[1].Point.Row) ? // Same row
+ Puzzle.Rows[gtTwo[0].Point.Row] : Puzzle.Columns[gtTwo[0].Point.Column])
+ .Where(c => c.Candidates.ContainsAny(others) && !c.Candidates.ContainsAny(Utils.OneToNine.Except(others)));
+ if (nSubset.Count() != others.Count() - 1)
+ {
+ continue;
+ }
+ if (!Cell.ChangeCandidates(nSubset.Union(gtTwo).Select(c => c.VisibleCells).IntersectAll(), others))
+ {
+ continue;
+ }
+ break;
+ }
+ case 4:
+ {
+ int[] remove = new int[1];
+ if (four[0].Point.BlockIndex == three[0].Point.BlockIndex)
+ {
+ if (Puzzle.Blocks[four[0].Point.BlockIndex].GetCellsWithCandidate(value1).Count() == 2)
+ {
+ remove[0] = value2;
+ }
+ else if (Puzzle.Blocks[four[0].Point.BlockIndex].GetCellsWithCandidate(value2).Count() == 2)
+ {
+ remove[0] = value1;
+ }
+ }
+ if (remove[0] != 0) // They share the same row/column but not the same block
+ {
+ if (three[0].Point.Column == three[0].Point.Column)
+ {
+ if (Puzzle.Columns[four[0].Point.Column].GetCellsWithCandidate(value1).Count() == 2)
+ {
+ remove[0] = value2;
+ }
+ else if (Puzzle.Columns[four[0].Point.Column].GetCellsWithCandidate(value2).Count() == 2)
+ {
+ remove[0] = value1;
+ }
+ }
+ else
+ {
+ if (Puzzle.Rows[four[0].Point.Row].GetCellsWithCandidate(value1).Count() == 2)
+ {
+ remove[0] = value2;
+ }
+ else if (Puzzle.Rows[four[0].Point.Row].GetCellsWithCandidate(value2).Count() == 2)
+ {
+ remove[0] = value1;
+ }
+ }
+ }
+ else
+ {
+ continue;
+ }
+ Cell.ChangeCandidates(cells.Except(l[2]), remove);
+ break;
+ }
+ case 5:
+ {
+ if (!three[0].Candidates.SetEquals(three[1].Candidates) || !three[1].Candidates.SetEquals(three[2].Candidates))
+ {
+ continue;
+ }
+ if (!Cell.ChangeCandidates(three.Select(c => c.VisibleCells).IntersectAll(), three[0].Candidates.Except(candidates)))
+ {
+ continue;
+ }
+ break;
+ }
+ case 6:
+ {
+ if (three[0].Point.Column == three[1].Point.Column)
+ {
+ continue;
+ }
+ int set;
+ if (c1.GetCellsWithCandidate(value1).Count() == 2 && c2.GetCellsWithCandidate(value1).Count() == 2 // Check if "v" only appears in the UR
+ && Puzzle.Rows[two[0].Point.Row].GetCellsWithCandidate(value1).Count() == 2
+ && Puzzle.Rows[two[1].Point.Row].GetCellsWithCandidate(value1).Count() == 2)
+ {
+ set = value1;
+ }
+ else if (c1.GetCellsWithCandidate(value2).Count() == 2 && c2.GetCellsWithCandidate(value2).Count() == 2
+ && Puzzle.Rows[two[0].Point.Row].GetCellsWithCandidate(value2).Count() == 2
+ && Puzzle.Rows[two[1].Point.Row].GetCellsWithCandidate(value2).Count() == 2)
+ {
+ set = value2;
+ }
+ else
+ {
+ continue;
+ }
+ two[0].Set(set);
+ two[1].Set(set);
+ break;
+ }
+ }
+
+ LogAction(TechniqueFormat("Unique rectangle", "{0}: {1}", cells.Print(), candidates.Print()), cells);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_XYChain.cs b/SudokuSolver/SolverTechniques/Solver_XYChain.cs
new file mode 100644
index 0000000..039e0ee
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_XYChain.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool XYChain()
+ {
+ bool Recursion(Cell startCell, List ignore, Cell currentCell, int theOneThatWillEndItAllBaybee, int mustFind)
+ {
+ ignore.Add(currentCell);
+ IEnumerable visible = currentCell.VisibleCells.Except(ignore);
+ foreach (Cell cell in visible)
+ {
+ if (cell.Candidates.Count != 2)
+ {
+ continue; // Must have two candidates
+ }
+ if (!cell.Candidates.Contains(mustFind))
+ {
+ continue; // Must have "mustFind"
+ }
+
+ int otherCandidate = cell.Candidates.Except(new int[] { mustFind }).Single();
+ // Check end condition
+ if (otherCandidate == theOneThatWillEndItAllBaybee && startCell != currentCell)
+ {
+ Cell[] commonVisibleWithStartCell = cell.VisibleCells.Intersect(startCell.VisibleCells).ToArray();
+ if (commonVisibleWithStartCell.Length > 0)
+ {
+ IEnumerable commonWithEndingCandidate = commonVisibleWithStartCell.Where(c => c.Candidates.Contains(theOneThatWillEndItAllBaybee));
+ if (Cell.ChangeCandidates(commonWithEndingCandidate, theOneThatWillEndItAllBaybee))
+ {
+ ignore.Remove(startCell); // Remove here because we're now using "ignore" as "semiCulprits" and exiting
+ var culprits = new Cell[] { startCell, cell };
+ LogAction(TechniqueFormat("XY-Chain", "{0}-{1}: {2}", culprits.Print(), ignore.SingleOrMultiToString(), theOneThatWillEndItAllBaybee), culprits, ignore);
+ return true;
+ }
+ }
+ }
+ // Loop again
+ if (Recursion(startCell, ignore, cell, theOneThatWillEndItAllBaybee, otherCandidate))
+ {
+ return true;
+ }
+ }
+ ignore.Remove(currentCell);
+ return false;
+ }
+
+ for (int x = 0; x < 9; x++)
+ {
+ for (int y = 0; y < 9; y++)
+ {
+ Cell cell = Puzzle[x, y];
+ if (cell.Candidates.Count != 2)
+ {
+ continue; // Must have two candidates
+ }
+ var ignore = new List();
+ int start1 = cell.Candidates.ElementAt(0);
+ int start2 = cell.Candidates.ElementAt(1);
+ if (Recursion(cell, ignore, cell, start1, start2) || Recursion(cell, ignore, cell, start2, start1))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_XYZWing.cs b/SudokuSolver/SolverTechniques/Solver_XYZWing.cs
new file mode 100644
index 0000000..ece493c
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_XYZWing.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool XYZWing()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (XYZWing_Find(Puzzle.Rows[i]) || XYZWing_Find(Puzzle.Columns[i]))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ private bool XYZWing_Find(Region region)
+ {
+ bool changed = false;
+ Cell[] cells2 = region.Where(c => c.Candidates.Count == 2).ToArray();
+ Cell[] cells3 = region.Where(c => c.Candidates.Count == 3).ToArray();
+ if (cells2.Length > 0 && cells3.Length > 0)
+ {
+ for (int j = 0; j < cells2.Length; j++)
+ {
+ Cell c2 = cells2[j];
+ for (int k = 0; k < cells3.Length; k++)
+ {
+ Cell c3 = cells3[k];
+ if (c2.Candidates.Intersect(c3.Candidates).Count() != 2)
+ {
+ continue;
+ }
+
+ IEnumerable c3Sees = c3.VisibleCells.Except(region)
+ .Where(c => c.Candidates.Count == 2 // If it has 2 candidates
+ && c.Candidates.Intersect(c3.Candidates).Count() == 2 // Shares them both with p3
+ && c.Candidates.Intersect(c2.Candidates).Count() == 1); // And shares one with p2
+ foreach (Cell c2_2 in c3Sees)
+ {
+ IEnumerable allSee = c2.VisibleCells.Intersect(c3.VisibleCells).Intersect(c2_2.VisibleCells);
+ int allHave = c2.Candidates.Intersect(c3.Candidates).Intersect(c2_2.Candidates).Single(); // Will be 1 Length
+ if (Cell.ChangeCandidates(allSee, allHave))
+ {
+ var culprits = new Cell[] { c2, c3, c2_2 };
+ LogAction(TechniqueFormat("XYZ-Wing", "{0}: {1}", culprits.Print(), allHave), culprits);
+ changed = true;
+ }
+ }
+ }
+ }
+ }
+ return changed;
+ }
+}
diff --git a/SudokuSolver/SolverTechniques/Solver_YWing.cs b/SudokuSolver/SolverTechniques/Solver_YWing.cs
new file mode 100644
index 0000000..b54e404
--- /dev/null
+++ b/SudokuSolver/SolverTechniques/Solver_YWing.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private bool YWing()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ if (YWing_Find(Puzzle.Rows[i]) || YWing_Find(Puzzle.Columns[i]))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ private bool YWing_Find(Region region)
+ {
+ Cell[] cells = region.Where(c => c.Candidates.Count == 2).ToArray();
+ if (cells.Length > 1)
+ {
+ for (int j = 0; j < cells.Length; j++)
+ {
+ Cell c1 = cells[j];
+ for (int k = j + 1; k < cells.Length; k++)
+ {
+ Cell c2 = cells[k];
+ IEnumerable inter = c1.Candidates.Intersect(c2.Candidates);
+ if (inter.Count() != 1)
+ {
+ continue;
+ }
+
+ int other1 = c1.Candidates.Except(inter).ElementAt(0);
+ int other2 = c2.Candidates.Except(inter).ElementAt(0);
+
+ var a = new Cell[] { c1, c2 };
+ foreach (Cell cell in a)
+ {
+ IEnumerable c3a = cell.VisibleCells.Except(cells).Where(c => c.Candidates.Count == 2 && c.Candidates.Intersect(new int[] { other1, other2 }).Count() == 2);
+ if (c3a.Count() == 1) // Example: p1 and p3 see each other, so remove similarities from p2 and p3
+ {
+ Cell c3 = c3a.ElementAt(0);
+ Cell cOther = a.Single(c => c != cell);
+ IEnumerable commonCells = cOther.VisibleCells.Intersect(c3.VisibleCells);
+ int candidate = cOther.Candidates.Intersect(c3.Candidates).Single(); // Will just be 1 candidate
+ if (Cell.ChangeCandidates(commonCells, candidate))
+ {
+ var culprits = new Cell[] { c1, c2, c3 };
+ LogAction(TechniqueFormat("Y-Wing", "{0}: {1}", culprits.Print(), candidate), culprits);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/SudokuSolver/Solver_Techniques.cs b/SudokuSolver/Solver_Techniques.cs
new file mode 100644
index 0000000..2e85d37
--- /dev/null
+++ b/SudokuSolver/Solver_Techniques.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace Kermalis.SudokuSolver;
+
+partial class Solver
+{
+ private sealed class SolverTechnique
+ {
+ public Func Function { get; }
+ /// Currently unused.
+ public string Url { get; }
+
+ public SolverTechnique(Func function, string url)
+ {
+ Function = function;
+ Url = url;
+ }
+ }
+
+ private static ReadOnlySpan TupleStr => new string[5] { string.Empty, "single", "pair", "triple", "quadruple" };
+
+ private readonly SolverTechnique[] _techniques;
+
+ private readonly Cell[] _cellCache = new Cell[20];
+ private readonly int[] _intCache = new int[20];
+
+ private SolverTechnique[] InitSolverTechniques()
+ {
+ return [
+ new SolverTechnique(HiddenSingle, "Hidden single"),
+ new SolverTechnique(NakedPair, "https://hodoku.sourceforge.net/en/tech_naked.php#n2"),
+ new SolverTechnique(HiddenPair, "https://hodoku.sourceforge.net/en/tech_hidden.php#h2"),
+ new SolverTechnique(LockedCandidate, "https://hodoku.sourceforge.net/en/tech_intersections.php#lc1"),
+ new SolverTechnique(PointingTuple, "https://hodoku.sourceforge.net/en/tech_intersections.php#lc1"),
+ new SolverTechnique(NakedTriple, "https://hodoku.sourceforge.net/en/tech_naked.php#n3"),
+ new SolverTechnique(HiddenTriple, "https://hodoku.sourceforge.net/en/tech_hidden.php#h3"),
+ new SolverTechnique(XWing, "https://hodoku.sourceforge.net/en/tech_fishb.php#bf2"),
+ new SolverTechnique(Swordfish, "https://hodoku.sourceforge.net/en/tech_fishb.php#bf3"),
+ new SolverTechnique(YWing, "https://www.sudokuwiki.org/Y_Wing_Strategy"),
+ new SolverTechnique(XYZWing, "https://www.sudokuwiki.org/XYZ_Wing"),
+ new SolverTechnique(XYChain, "https://www.sudokuwiki.org/XY_Chains"),
+ new SolverTechnique(NakedQuadruple, "https://hodoku.sourceforge.net/en/tech_naked.php#n4"),
+ new SolverTechnique(HiddenQuadruple, "https://hodoku.sourceforge.net/en/tech_hidden.php#h4"),
+ new SolverTechnique(Jellyfish, "https://hodoku.sourceforge.net/en/tech_fishb.php#bf4"),
+ new SolverTechnique(UniqueRectangle, "https://hodoku.sourceforge.net/en/tech_ur.php"),
+ new SolverTechnique(HiddenRectangle, "https://hodoku.sourceforge.net/en/tech_ur.php#hr"),
+ new SolverTechnique(AvoidableRectangle, "https://hodoku.sourceforge.net/en/tech_ur.php#ar"),
+ ];
+ }
+}
diff --git a/SudokuSolver/SudokuSolver.csproj b/SudokuSolver/SudokuSolver.csproj
index 5e1a49e..4cfba77 100644
--- a/SudokuSolver/SudokuSolver.csproj
+++ b/SudokuSolver/SudokuSolver.csproj
@@ -1,22 +1,11 @@
- net7.0-windows
- WinExe
+ net8.0
+ Library
latest
Kermalis.SudokuSolver
- Kermalis.SudokuSolver.Program
enable
- true
- ..\Build
-
- Kermalis
- Kermalis
- SudokuSolver
- SudokuSolver
- SudokuSolver
- Properties\Icon.ico
- False
\ No newline at end of file
diff --git a/SudokuSolver/Core/Utils.cs b/SudokuSolver/Utils.cs
similarity index 65%
rename from SudokuSolver/Core/Utils.cs
rename to SudokuSolver/Utils.cs
index 77a2ac7..f8e237f 100644
--- a/SudokuSolver/Core/Utils.cs
+++ b/SudokuSolver/Utils.cs
@@ -3,24 +3,27 @@
using System.Collections.ObjectModel;
using System.Linq;
-namespace Kermalis.SudokuSolver.Core;
+namespace Kermalis.SudokuSolver;
internal static class Utils
{
public static ReadOnlyCollection OneToNine { get; } = new ReadOnlyCollection(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
+ public static ReadOnlySpan OneToNineSpan => new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
- public static void GetColumnInBlock(this Cell[] block, int x, Span column)
+ /// outCol must be 3 length
+ public static void GetColumnInBlock(this Cell[] block, int x, Span outCol)
{
- for (int i = 0; i < 3; i++)
+ for (int col = 0; col < 3; col++)
{
- column[i] = block[(x * 3) + i];
+ outCol[col] = block[(x * 3) + col];
}
}
- public static void GetRowInBlock(this Cell[] block, int y, Span row)
+ /// outCol must be 3 length
+ public static void GetRowInBlock(this Cell[] block, int y, Span outRow)
{
- for (int i = 0; i < 3; i++)
+ for (int row = 0; row < 3; row++)
{
- row[i] = block[(i * 3) + y];
+ outRow[row] = block[(row * 3) + y];
}
}
public static Cell[] GetColumnInBlock(this Cell[] block, int x)
@@ -109,4 +112,39 @@ public static string Print(this IEnumerable source)
{
return "( " + string.Join(", ", source) + " )";
}
+ public static string Print(this T[] source)
+ {
+ return "( " + string.Join(", ", source) + " )"; // TODO: Deal with span allocs here. Have PrintCandidates and PrintCells
+ }
+ /*public static string PrintCells(ReadOnlySpan cells)
+ {
+
+ }
+ public static string PrintCandidates(ReadOnlySpan candidates)
+ {
+
+ }*/
+
+ public static int SimpleIndexOf(this ReadOnlySpan cells, Cell cell)
+ {
+ for (int i = 0; i < cells.Length; i++)
+ {
+ if (cells[i].Point == cell.Point)
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+ public static int SimpleIndexOf(this ReadOnlySpan candidates, int can)
+ {
+ for (int i = 0; i < candidates.Length; i++)
+ {
+ if (candidates[i] == can)
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
}
diff --git a/SudokuSolverTests/SudokuSolverTests.csproj b/SudokuSolverTests/SudokuSolverTests.csproj
new file mode 100644
index 0000000..16193f5
--- /dev/null
+++ b/SudokuSolverTests/SudokuSolverTests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ latest
+ Kermalis.SudokuSolver.Tests
+ enable
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
\ No newline at end of file
diff --git a/SudokuSolverTests/TestSolverTechniques.cs b/SudokuSolverTests/TestSolverTechniques.cs
new file mode 100644
index 0000000..bcbddf4
--- /dev/null
+++ b/SudokuSolverTests/TestSolverTechniques.cs
@@ -0,0 +1,36 @@
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Kermalis.SudokuSolver.Tests;
+
+[Collection("Utils")]
+public sealed class TestSolverTechniques
+{
+ private readonly TestUtils _utils;
+
+ public TestSolverTechniques(TestUtils utils, ITestOutputHelper output)
+ {
+ _utils = utils;
+ _utils.SetOutputHelper(output);
+ }
+
+ [Fact]
+ public void Jellyfish()
+ {
+ string[] puzzleText =
+ [
+ "2-------3",
+ "-8--3--5-",
+ "--34-21--",
+ "--12-54--",
+ "----9----",
+ "--93-86--",
+ "--25-69--",
+ "-9--2--7-",
+ "4-------1"
+ ];
+
+ Solver solver = _utils.CreateSolver(puzzleText);
+ _utils.AssertSolvedCorrectly(solver);
+ }
+}
\ No newline at end of file
diff --git a/SudokuSolverTests/TestUtils.cs b/SudokuSolverTests/TestUtils.cs
new file mode 100644
index 0000000..62d71eb
--- /dev/null
+++ b/SudokuSolverTests/TestUtils.cs
@@ -0,0 +1,52 @@
+using System;
+using System.ComponentModel;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Kermalis.SudokuSolver.Tests;
+
+[CollectionDefinition("Utils")]
+public sealed class TestUtilsCollection : ICollectionFixture
+{
+ //
+}
+
+public sealed class TestUtils
+{
+ private ITestOutputHelper _output = null!;
+
+ public void SetOutputHelper(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ public Solver CreateSolver(ReadOnlySpan puzzleText)
+ {
+ var solver = new Solver(Puzzle.Parse(puzzleText));
+ _output.WriteLine(solver.Puzzle.ToStringFancy());
+ _output.WriteLine(string.Empty);
+ solver.Actions.ListChanged += Actions_ListChanged;
+ return solver;
+ }
+ public void AssertSolvedCorrectly(Solver solver)
+ {
+ Assert.True(solver.TrySolve());
+ Assert.False(solver.Puzzle.CheckForErrors());
+
+ _output.WriteLine(string.Empty);
+ _output.WriteLine(solver.Puzzle.ToStringFancy());
+ }
+
+ private void Actions_ListChanged(object? sender, ListChangedEventArgs e)
+ {
+ if (e.ListChangedType == ListChangedType.ItemAdded)
+ {
+ var list = (BindingList)sender!;
+ _output.WriteLine(list[e.NewIndex]);
+ }
+ }
+
+ private void Actions_AddingNew(object? sender, AddingNewEventArgs e)
+ {
+ }
+}
\ No newline at end of file
diff --git a/SudokuSolver/UI/MainWindow.Designer.cs b/SudokuSolverWinForms/MainWindow.Designer.cs
similarity index 100%
rename from SudokuSolver/UI/MainWindow.Designer.cs
rename to SudokuSolverWinForms/MainWindow.Designer.cs
diff --git a/SudokuSolver/UI/MainWindow.cs b/SudokuSolverWinForms/MainWindow.cs
similarity index 60%
rename from SudokuSolver/UI/MainWindow.cs
rename to SudokuSolverWinForms/MainWindow.cs
index 9978749..7315778 100644
--- a/SudokuSolver/UI/MainWindow.cs
+++ b/SudokuSolverWinForms/MainWindow.cs
@@ -1,15 +1,12 @@
-using Kermalis.SudokuSolver.Core;
-using System;
-using System.ComponentModel;
+using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
namespace Kermalis.SudokuSolver.UI;
-internal sealed partial class MainWindow : Form
+internal sealed partial class MainWindow : Form // TODO: Readme, alloc, tests, fix custom puzzle
{
- private Stopwatch? _stopwatch;
private Solver? _solver;
public MainWindow()
@@ -33,26 +30,19 @@ private void ChangeState(bool solveButtonState, bool saveState)
_saveAsToolStripMenuItem.Enabled = saveState;
}
- private void ChangePuzzle(string puzzleName, bool solveButtonState)
+ private void ChangePuzzle(Solver newSolver, string puzzleName, bool solveButtonState)
{
+ _solver = newSolver;
ChangeState(solveButtonState, false);
_puzzleLabel.Text = puzzleName + " Puzzle";
_statusLabel.Text = string.Empty;
- _logList.DataSource = _solver!.Puzzle.Actions;
- _sudokuBoard.SetBoard(_solver.Puzzle);
+ _logList.DataSource = _solver.Actions;
+ _sudokuBoard.SetSolver(_solver);
}
private void NewPuzzle(object? sender, EventArgs e)
{
- int[][] board = new int[9][];
- for (int i = 0; i < 9; i++)
- {
- board[i] = new int[9];
- }
- _solver = new Solver(new Puzzle(board, true));
-
- ChangePuzzle("Custom", false);
- _solver.Puzzle.LogAction("Custom puzzle created");
+ ChangePuzzle(Solver.CreateCustomPuzzle(), "Custom", false);
MessageBox.Show("A custom puzzle has been created. Click cells to type in values.", Text);
}
@@ -70,7 +60,7 @@ private void OpenPuzzle(object? sender, EventArgs e)
Puzzle puzzle;
try
{
- puzzle = Puzzle.Load(d.FileName);
+ puzzle = Puzzle.Parse(File.ReadAllLines(d.FileName));
}
catch (InvalidDataException)
{
@@ -83,8 +73,7 @@ private void OpenPuzzle(object? sender, EventArgs e)
return;
}
- _solver = new Solver(puzzle);
- ChangePuzzle(Path.GetFileNameWithoutExtension(d.FileName), true);
+ ChangePuzzle(new Solver(puzzle), Path.GetFileNameWithoutExtension(d.FileName), true);
}
}
@@ -99,7 +88,7 @@ private void SavePuzzle(object? sender, EventArgs e)
if (d.ShowDialog() == DialogResult.OK)
{
- _solver!.Puzzle.Save(d.FileName);
+ File.WriteAllText(d.FileName, _solver!.Puzzle.ToString());
MessageBox.Show("Puzzle saved.", Text);
}
}
@@ -110,32 +99,15 @@ private void SolvePuzzle(object? sender, EventArgs e)
// Clear solver's guesses on a custom puzzle
if (_solver!.Puzzle.IsCustom)
{
- for (int x = 0; x < 9; x++)
- {
- for (int y = 0; y < 9; y++)
- {
- if (_solver.Puzzle[x, y].Value != _solver.Puzzle[x, y].OriginalValue)
- {
- _solver.Puzzle[x, y].Set(Cell.EMPTY_VALUE);
- }
- }
- }
+ _solver.Puzzle.Reset();
}
- _stopwatch = new Stopwatch();
- var bw = new BackgroundWorker();
- bw.DoWork += _solver.DoWork;
- bw.RunWorkerCompleted += SolverFinished;
- _stopwatch.Start();
- bw.RunWorkerAsync();
- }
-
- private void SolverFinished(object? sender, RunWorkerCompletedEventArgs e)
- {
- _stopwatch!.Stop();
- _statusLabel.Text = string.Format("Solver finished in {0} seconds.", _stopwatch.Elapsed.TotalSeconds);
- _solver!.Puzzle.LogAction(string.Format("Solver {0} the puzzle", ((bool)e.Result!) ? "completed" : "failed"));
- _logList.SelectedIndex = _solver.Puzzle.Actions.Count - 1;
+ var sw = new Stopwatch();
+ sw.Start();
+ _solver.TrySolve();
+ sw.Stop();
+ _statusLabel.Text = string.Format("Solver finished in {0} seconds.", sw.Elapsed.TotalSeconds);
+ _logList.SelectedIndex = _solver.Actions.Count - 1;
_logList.Select();
}
diff --git a/SudokuSolver/UI/MainWindow.resx b/SudokuSolverWinForms/MainWindow.resx
similarity index 100%
rename from SudokuSolver/UI/MainWindow.resx
rename to SudokuSolverWinForms/MainWindow.resx
diff --git a/SudokuSolver/Program.cs b/SudokuSolverWinForms/Program.cs
similarity index 74%
rename from SudokuSolver/Program.cs
rename to SudokuSolverWinForms/Program.cs
index ec8ef03..ec5c0eb 100644
--- a/SudokuSolver/Program.cs
+++ b/SudokuSolverWinForms/Program.cs
@@ -1,8 +1,7 @@
-using Kermalis.SudokuSolver.UI;
-using System;
+using System;
using System.Windows.Forms;
-namespace Kermalis.SudokuSolver;
+namespace Kermalis.SudokuSolver.UI;
internal static class Program
{
diff --git a/SudokuSolver/Properties/Icon.ico b/SudokuSolverWinForms/Properties/Icon.ico
similarity index 100%
rename from SudokuSolver/Properties/Icon.ico
rename to SudokuSolverWinForms/Properties/Icon.ico
diff --git a/SudokuSolver/Properties/Icon.png b/SudokuSolverWinForms/Properties/Icon.png
similarity index 100%
rename from SudokuSolver/Properties/Icon.png
rename to SudokuSolverWinForms/Properties/Icon.png
diff --git a/SudokuSolver/UI/SudokuBoard.cs b/SudokuSolverWinForms/SudokuBoard.cs
similarity index 71%
rename from SudokuSolver/UI/SudokuBoard.cs
rename to SudokuSolverWinForms/SudokuBoard.cs
index 6d88f58..27cb672 100644
--- a/SudokuSolver/UI/SudokuBoard.cs
+++ b/SudokuSolverWinForms/SudokuBoard.cs
@@ -1,5 +1,4 @@
-using Kermalis.SudokuSolver.Core;
-using System;
+using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
@@ -11,19 +10,19 @@ internal sealed class SudokuBoard : UserControl
private const int SPACE_BEFORE_GRID = 20;
private const int NO_SNAPSHOT = -1;
- private readonly Brush _changedText = Brushes.DodgerBlue;
- private readonly Brush _candidateText = Brushes.Crimson;
- private readonly Brush _culpritChangedHighlight = Brushes.Plum;
- private readonly Brush _culpritHighlight = Brushes.Pink;
- private readonly Brush _semiCulpritChangedHighlight = Brushes.CornflowerBlue;
- private readonly Brush _semiCulpritHighlight = Brushes.Aquamarine;
- private readonly Brush _changedHighlight = Brushes.Cornsilk;
+ private static readonly Brush _changedText = Brushes.DodgerBlue;
+ private static readonly Brush _candidateText = Brushes.Crimson;
+ private static readonly Brush _culpritChangedHighlight = Brushes.Plum;
+ private static readonly Brush _culpritHighlight = Brushes.Pink;
+ private static readonly Brush _semiCulpritChangedHighlight = Brushes.CornflowerBlue;
+ private static readonly Brush _semiCulpritHighlight = Brushes.Aquamarine;
+ private static readonly Brush _changedHighlight = Brushes.Cornsilk;
public delegate void CellChangedEventHandler(Cell cell);
public event CellChangedEventHandler? CellChanged;
private Cell? _selectedCell;
- private Puzzle? _puzzle;
+ private Solver? _solver;
private bool _showCandidates;
private int _snapshotIndex;
@@ -49,11 +48,13 @@ private void SudokuBoard_Paint(object? sender, PaintEventArgs e)
{
Font f = Font;
var fMini = new Font(f.FontFamily, f.Size / 1.75f);
- float rWidth = Width - SPACE_BEFORE_GRID, rHeight = Height - SPACE_BEFORE_GRID;
+ float rWidth = Width - SPACE_BEFORE_GRID;
+ float rHeight = Height - SPACE_BEFORE_GRID;
e.Graphics.DrawRectangle(Pens.Black, SPACE_BEFORE_GRID, SPACE_BEFORE_GRID, rWidth - 1, rHeight - 1);
- float w = rWidth / 3f, h = rHeight / 3f;
+ float w = rWidth / 3f;
+ float h = rHeight / 3f;
bool b = true;
for (int x = 0; x < 3; x++)
{
@@ -76,23 +77,29 @@ private void SudokuBoard_Paint(object? sender, PaintEventArgs e)
{
float yoff = h * y;
e.Graphics.DrawRectangle(Pens.Black, xoff + SPACE_BEFORE_GRID, yoff + SPACE_BEFORE_GRID, w, h);
- if (_puzzle is null)
+ if (_solver is null)
{
continue;
}
- int val = _puzzle[x, y].Value;
- IEnumerable candidates = _puzzle[x, y].Candidates;
+ Cell cell = _solver.Puzzle[x, y];
+ int val;
+ IEnumerable candidates;
- if (_snapshotIndex != NO_SNAPSHOT && _snapshotIndex < _puzzle[x, y].Snapshots.Count)
+ if (_snapshotIndex == NO_SNAPSHOT || _snapshotIndex >= cell.Snapshots.Count)
{
- CellSnapshot s = _puzzle[x, y].Snapshots[_snapshotIndex];
+ val = cell.Value;
+ candidates = cell.Candidates;
+ }
+ else
+ {
+ CellSnapshot s = cell.Snapshots[_snapshotIndex];
val = s.Value;
candidates = s.Candidates;
int xxoff = x % 3 == 0 ? 1 : 0, yyoff = y % 3 == 0 ? 1 : 0; // MATH
int exoff = x % 3 == 2 ? 1 : 0, eyoff = y % 3 == 2 ? 1 : 0;
var rect = new RectangleF(xoff + SPACE_BEFORE_GRID + 1 + xxoff, yoff + SPACE_BEFORE_GRID + 1 + yyoff, w - 1 - xxoff - exoff, h - 1 - yyoff - eyoff);
- bool changed = _snapshotIndex - 1 >= 0 && !new HashSet(s.Candidates).SetEquals(_puzzle[x, y].Snapshots[_snapshotIndex - 1].Candidates);
+ bool changed = _snapshotIndex - 1 >= 0 && !new HashSet(s.Candidates).SetEquals(cell.Snapshots[_snapshotIndex - 1].Candidates);
Brush? brush = null;
if (changed)
{
@@ -124,20 +131,20 @@ private void SudokuBoard_Paint(object? sender, PaintEventArgs e)
}
var point = new PointF(xoff + f.Size / 1.5f + SPACE_BEFORE_GRID, yoff + f.Size / 2.25f + SPACE_BEFORE_GRID);
- if (_selectedCell is not null && _selectedCell.Point.X == x && _selectedCell.Point.Y == y)
+ if (_selectedCell is not null && _selectedCell.Point.Equals(x, y))
{
e.Graphics.DrawString("_", f, Brushes.Crimson, point);
}
- if (val != 0)
+ if (val != Cell.EMPTY_VALUE)
{
- e.Graphics.DrawString(val.ToString(), f, val == _puzzle[x, y].OriginalValue ? Brushes.Black : _changedText, point);
+ e.Graphics.DrawString(val.ToString(), f, val == cell.OriginalValue ? Brushes.Black : _changedText, point);
}
else if (_showCandidates)
{
foreach (int c in candidates)
{
- float stringX = xoff + fMini.Size / 4 + (((c - 1) % 3) * (w / 3)) + SPACE_BEFORE_GRID;
- float stringY = yoff + (((c - 1) / 3) * (h / 3)) + SPACE_BEFORE_GRID;
+ float stringX = xoff + fMini.Size / 4 + ((c - 1) % 3 * (w / 3)) + SPACE_BEFORE_GRID;
+ float stringY = yoff + ((c - 1) / 3 * (h / 3)) + SPACE_BEFORE_GRID;
e.Graphics.DrawString(c.ToString(), fMini, _candidateText, stringX, stringY);
}
}
@@ -147,14 +154,15 @@ private void SudokuBoard_Paint(object? sender, PaintEventArgs e)
private Cell? GetCellFromMouseLocation(Point location)
{
- if (_puzzle is null || !_puzzle.IsCustom || location.X < SPACE_BEFORE_GRID || location.Y < SPACE_BEFORE_GRID)
+ // TODO: There is a crash on the right side of the board. Should have a better solution
+ if (_solver is null || !_solver.Puzzle.IsCustom || location.X < SPACE_BEFORE_GRID || location.Y < SPACE_BEFORE_GRID)
{
return null;
}
int col = (location.X - SPACE_BEFORE_GRID) / ((Width - SPACE_BEFORE_GRID) / 9);
int row = (location.Y - SPACE_BEFORE_GRID) / ((Height - SPACE_BEFORE_GRID) / 9);
- return _puzzle[col, row];
+ return _solver.Puzzle[col, row];
}
private void SudokuBoard_MouseMove(object? sender, MouseEventArgs e)
@@ -177,8 +185,7 @@ private void SudokuBoard_KeyPress(object? sender, KeyPressEventArgs e)
if ((e.KeyChar == '0' && _selectedCell.Value != Cell.EMPTY_VALUE) || (e.KeyChar > '0' && e.KeyChar <= '9'))
{
- _selectedCell.ChangeOriginalValue(e.KeyChar - '0');
- _puzzle!.LogAction(Puzzle.TechniqueFormat("Changed cell", _selectedCell.ToString()), _selectedCell, (Cell?)null);
+ _solver!.SetOriginalCellValue(_selectedCell, e.KeyChar - '0');
CellChanged?.Invoke(_selectedCell);
}
@@ -205,9 +212,9 @@ public void ReDraw(bool showCandidates, int snapshot = NO_SNAPSHOT)
Invalidate();
}
- public void SetBoard(Puzzle newBoard)
+ public void SetSolver(Solver newSolver)
{
- _puzzle = newBoard;
+ _solver = newSolver;
ReDraw(false);
}
}
diff --git a/SudokuSolverWinForms/SudokuSolverWinForms.csproj b/SudokuSolverWinForms/SudokuSolverWinForms.csproj
new file mode 100644
index 0000000..52c2722
--- /dev/null
+++ b/SudokuSolverWinForms/SudokuSolverWinForms.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0-windows
+ WinExe
+ latest
+ Kermalis.SudokuSolver.UI
+ Kermalis.SudokuSolver.UI.Program
+ enable
+ true
+ ..\Build
+
+ Kermalis
+ Kermalis
+ SudokuSolver
+ SudokuSolver
+ Properties\Icon.ico
+ False
+
+
+
+
+
+
+
\ No newline at end of file
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |