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