Even though it's the first time I'm writing something this "big", it feels like I know C# quite well (it is very similar to Java after all). It's been nice to learn LINQ also and I am very impressed by the features (which is just like Steams in Java 8), and perhaps I have overused it here (if it's possible to do that). ### Class summary - SudokuFactory: Contains static methods to create some Sudoku variations - SudokuBoard: Contains collection of `SudokuRule` and of `SudokuTile` - SudokuRule: Whether it's a box, a line, a row, or something entirely different doesn't matter. Contains a collection of `SudokuTile` that must be unique. - SudokuTile: Each tile in the puzzle. Can be "blocked" (like a hole in the puzzle), remembers it's `possibleValues`, and also contains a value (0 is used for tiles without a value) - SudokuProgress: Used to know what the progress of a solving step was. - Program: Main starting point. Contains tests for seven different Sudokus. All have been verified to be solved correctly. Since this is the first time I'm using C# and LINQ, please tell me anything. All suggestions welcome. Except for the fact that the method `box` should be called `Box`. I'd be especially interested in cases where I could simplify some of the LINQ usage (trust me, there is a lot). I hope you are able to follow all the LINQ queries. I have tried to put some short comments where needed to explain what is happening. If you want an explanation for some part, post a comment and I will explain. As usual, I have a tendency to make the challenge into something super-flexible with support for a whole lot of more or less unnecessary things. Some of the possible puzzles that this code can solve is: - A hard classic [9x9 Sudoku with 3x3 boxes](https://codereview.meta.stackexchange.com/questions/1245/weekend-challenge-3/1248#1248) that requires more advanced techniques (or in my case, more or less "brute force" by trial and error) - [Nonomino](http://en.wikipedia.org/wiki/File:A_nonomino_sudoku.svg) - [HyperSudoku](http://en.wikipedia.org/wiki/File:Oceans_Hypersudoku18_Puzzle.svg) - [Samurai Sudoku](http://www.freesamuraisudoku.com/1001HardSamuraiSudokus.aspx?puzzle=42) - A classic Sudoku of any size with any number of boxes and size of boxes (only completely tested on 9x9 with 3x3 boxes and 4x4 with 2x2 boxes but any sizes should be possible) These images are puzzles that are tested and solved in the below code: ![The Weekend Challenge Sudoku][1] ![Sudoku with "weird" boxes][2] ![Sudoku with additional boxes][3] ![Samurai Sudoku][4] One known issue with the implementation is if you would input an empty puzzle, then it would work for years to find all the possible combinations for it. SudokuProgress public enum SudokuProgress { FAILED, NO_PROGRESS, PROGRESS } SudokuTile public class SudokuTile { internal static SudokuProgress CombineSolvedState(SudokuProgress a, SudokuProgress b) { if (a == SudokuProgress.FAILED) return a; if (a == SudokuProgress.NO_PROGRESS) return b; if (a == SudokuProgress.PROGRESS) return b == SudokuProgress.FAILED ? b : a; throw new InvalidOperationException("Invalid value for a"); } public const int CLEARED = 0; private int _maxValue; private int _value; private int _x; private int _y; private ISet<int> possibleValues; private bool _blocked; public SudokuTile(int x, int y, int maxValue) { _x = x; _y = y; _blocked = false; _maxValue = maxValue; possibleValues = new HashSet<int>(); _value = 0; } public int Value { get { return _value; } set { if (value > _maxValue) throw new ArgumentOutOfRangeException("SudokuTile Value cannot be greater than " + _maxValue.ToString() + ". Was " + value); if (value < CLEARED) throw new ArgumentOutOfRangeException("SudokuTile Value cannot be zero or smaller. Was " + value); _value = value; } } public bool HasValue { get { return Value != CLEARED; } } public string ToStringSimple() { return Value.ToString(); } public override string ToString() { return String.Format("Value {0} at pos {1}, {2}. ", Value, _x, _y, possibleValues.Count); } internal void ResetPossibles() { possibleValues.Clear(); foreach (int i in Enumerable.Range(1, _maxValue)) { if (!HasValue || Value == i) possibleValues.Add(i); } } public void Block() { _blocked = true; } internal void Fix(int value, string reason) { Console.WriteLine("Fixing {0} on pos {1}, {2}: {3}", value, _x, _y, reason); Value = value; ResetPossibles(); } internal SudokuProgress RemovePossibles(IEnumerable<int> existingNumbers) { if (_blocked) return SudokuProgress.NO_PROGRESS; // Takes the current possible values and removes the ones existing in `existingNumbers` possibleValues = new HashSet<int>(possibleValues.Where(x => !existingNumbers.Contains(x))); SudokuProgress result = SudokuProgress.NO_PROGRESS; if (possibleValues.Count == 1) { Fix(possibleValues.First(), "Only one possibility"); result = SudokuProgress.PROGRESS; } if (possibleValues.Count == 0) return SudokuProgress.FAILED; return result; } public bool IsValuePossible(int i) { return possibleValues.Contains(i); } public int X { get { return _x; } } public int Y { get { return _y; } } public bool IsBlocked { get { return _blocked; } } // A blocked field can not contain a value -- used for creating 'holes' in the map public int PossibleCount { get { return IsBlocked ? 1 : possibleValues.Count; } } } SudokuRule public class SudokuRule : IEnumerable<SudokuTile> { internal SudokuRule(IEnumerable<SudokuTile> tiles, string description) { _tiles = new HashSet<SudokuTile>(tiles); _description = description; } private ISet<SudokuTile> _tiles; private string _description; public bool CheckValid() { var filtered = _tiles.Where(tile => tile.HasValue); var groupedByValue = filtered.GroupBy(tile => tile.Value); return groupedByValue.All(group => group.Count() == 1); } public bool CheckComplete() { return _tiles.All(tile => tile.HasValue) && CheckValid(); } internal SudokuProgress RemovePossibles() { // Tiles that has a number already IEnumerable<SudokuTile> withNumber = _tiles.Where(tile => tile.HasValue); // Tiles without a number IEnumerable<SudokuTile> withoutNumber = _tiles.Where(tile => !tile.HasValue); // The existing numbers in this rule IEnumerable<int> existingNumbers = new HashSet<int>(withNumber.Select(tile => tile.Value).Distinct().ToList()); SudokuProgress result = SudokuProgress.NO_PROGRESS; foreach (SudokuTile tile in withoutNumber) result = SudokuTile.CombineSolvedState(result, tile.RemovePossibles(existingNumbers)); return result; } internal SudokuProgress CheckForOnlyOnePossibility() { // Check if there is only one number within this rule that can have a specific value IList<int> existingNumbers = _tiles.Select(tile => tile.Value).Distinct().ToList(); SudokuProgress result = SudokuProgress.NO_PROGRESS; foreach (int value in Enumerable.Range(1, _tiles.Count)) { if (existingNumbers.Contains(value)) // this rule already has the value, skip checking for it continue; var possibles = _tiles.Where(tile => !tile.HasValue && tile.IsValuePossible(value)).ToList(); if (possibles.Count == 0) return SudokuProgress.FAILED; if (possibles.Count == 1) { possibles.First().Fix(value, "Only possible in rule " + ToString()); result = SudokuProgress.PROGRESS; } } return result; } internal SudokuProgress Solve() { // If both are null, return null (indicating no change). If one is null, return the other. Else return result1 && result2 SudokuProgress result1 = RemovePossibles(); SudokuProgress result2 = CheckForOnlyOnePossibility(); return SudokuTile.CombineSolvedState(result1, result2); } public override string ToString() { return _description; } public IEnumerator<SudokuTile> GetEnumerator() { return _tiles.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } public string Description { get { return _description; } } } SudokuBoard: public class SudokuBoard { public SudokuBoard(SudokuBoard copy) { _maxValue = copy._maxValue; tiles = new SudokuTile[copy.Width, copy.Height]; CreateTiles(); // Copy the tile values foreach (var pos in SudokuFactory.box(Width, Height)) { tiles[pos.Item1, pos.Item2] = new SudokuTile(pos.Item1, pos.Item2, _maxValue); tiles[pos.Item1, pos.Item2].Value = copy.tiles[pos.Item1, pos.Item2].Value; } // Copy the rules foreach (SudokuRule rule in copy.rules) { var ruleTiles = new HashSet<SudokuTile>(); foreach (SudokuTile tile in rule) { ruleTiles.Add(tiles[tile.X, tile.Y]); } rules.Add(new SudokuRule(ruleTiles, rule.Description)); } } public SudokuBoard(int width, int height, int maxValue) { _maxValue = maxValue; tiles = new SudokuTile[width, height]; CreateTiles(); if (_maxValue == width || _maxValue == height) // If maxValue is not width or height, then adding line rules would be stupid SetupLineRules(); } public SudokuBoard(int width, int height) : this(width, height, Math.Max(width, height)) {} private int _maxValue; private void CreateTiles() { foreach (var pos in SudokuFactory.box(tiles.GetLength(0), tiles.GetLength(1))) { tiles[pos.Item1, pos.Item2] = new SudokuTile(pos.Item1, pos.Item2, _maxValue); } } private void SetupLineRules() { // Create rules for rows and columns for (int x = 0; x < Width; x++) { IEnumerable<SudokuTile> row = GetCol(x); rules.Add(new SudokuRule(row, "Row " + x.ToString())); } for (int y = 0; y < Height; y++) { IEnumerable<SudokuTile> col = GetRow(y); rules.Add(new SudokuRule(col, "Col " + y.ToString())); } } internal IEnumerable<SudokuTile> TileBox(int startX, int startY, int sizeX, int sizeY) { return from pos in SudokuFactory.box(sizeX, sizeY) select tiles[startX + pos.Item1, startY + pos.Item2]; } private IEnumerable<SudokuTile> GetRow(int row) { for (int i = 0; i < tiles.GetLength(0); i++) { yield return tiles[i, row]; } } private IEnumerable<SudokuTile> GetCol(int col) { for (int i = 0; i < tiles.GetLength(1); i++) { yield return tiles[col, i]; } } private ISet<SudokuRule> rules = new HashSet<SudokuRule>(); private SudokuTile[,] tiles; public int Width { get { return tiles.GetLength(0); } } public int Height { get { return tiles.GetLength(1); } } public void CreateRule(string description, params SudokuTile[] tiles) { rules.Add(new SudokuRule(tiles, description)); } public void CreateRule(string description, IEnumerable<SudokuTile> tiles) { rules.Add(new SudokuRule(tiles, description)); } public bool CheckValid() { return rules.All(rule => rule.CheckValid()); } public IEnumerable<SudokuBoard> Solve() { ResetSolutions(); SudokuProgress simplify = SudokuProgress.PROGRESS; while (simplify == SudokuProgress.PROGRESS) simplify = Simplify(); if (simplify == SudokuProgress.FAILED) yield break; // Find one of the values with the least number of alternatives, but that still has at least 2 alternatives var query = from rule in rules from tile in rule where tile.PossibleCount > 1 orderby tile.PossibleCount ascending select tile; SudokuTile chosen = query.FirstOrDefault(); if (chosen == null) { // The board has been completed, we're done! yield return this; yield break; } Console.WriteLine("SudokuTile: " + chosen.ToString()); foreach (var value in Enumerable.Range(1, _maxValue)) { // Iterate through all the valid possibles on the chosen square and pick a number for it if (!chosen.IsValuePossible(value)) continue; var copy = new SudokuBoard(this); copy.Tile(chosen.X, chosen.Y).Fix(value, "Trial and error"); foreach (var innerSolution in copy.Solve()) yield return innerSolution; } yield break; } public void Output() { for (int y = 0; y < tiles.GetLength(1); y++) { for (int x = 0; x < tiles.GetLength(0); x++) { Console.Write(tiles[x, y].ToStringSimple()); } Console.WriteLine(); } } public SudokuTile Tile(int x, int y) { return tiles[x, y]; } private int _rowAddIndex; public void AddRow(string s) { // Method for initializing a board from string for (int i = 0; i < s.Length; i++) { var tile = tiles[i, _rowAddIndex]; if (s[i] == '/') { tile.Block(); continue; } int value = s[i] == '.' ? 0 : (int)Char.GetNumericValue(s[i]); tile.Value = value; } _rowAddIndex++; } internal void ResetSolutions() { foreach (SudokuTile tile in tiles) tile.ResetPossibles(); } internal SudokuProgress Simplify() { SudokuProgress result = SudokuProgress.NO_PROGRESS; bool valid = CheckValid(); if (!valid) return SudokuProgress.FAILED; foreach (SudokuRule rule in rules) result = SudokuTile.CombineSolvedState(result, rule.Solve()); return result; } internal void AddBoxesCount(int boxesX, int boxesY) { int sizeX = Width / boxesX; int sizeY = Height / boxesY; var boxes = SudokuFactory.box(sizeX, sizeY); foreach (var pos in boxes) { IEnumerable<SudokuTile> boxTiles = TileBox(pos.Item1 * sizeX, pos.Item2 * sizeY, sizeX, sizeY); CreateRule("Box at (" + pos.Item1.ToString() + ", " + pos.Item2.ToString() + ")", boxTiles); } } internal void OutputRules() { foreach (var rule in rules) { Console.WriteLine(String.Join(",", rule) + " - " + rule.ToString()); } } } SudokuFactory: public class SudokuFactory { private const int DefaultSize = 9; private const int SamuraiAreas = 7; private const int BoxSize = 3; private const int HyperMargin = 1; public static IEnumerable<Tuple<int, int>> box(int sizeX, int sizeY) { foreach (int x in Enumerable.Range(0, sizeX)) { foreach (int y in Enumerable.Range(0, sizeY)) { yield return new Tuple<int, int>(x, y); } } } public static SudokuBoard Samurai() { SudokuBoard board = new SudokuBoard(SamuraiAreas*BoxSize, SamuraiAreas*BoxSize, DefaultSize); // Removed the empty areas where there are no tiles var queriesForBlocked = new List<IEnumerable<SudokuTile>>(); queriesForBlocked.Add(from pos in box(BoxSize, BoxSize*2) select board.Tile(pos.Item1 + DefaultSize, pos.Item2 )); queriesForBlocked.Add(from pos in box(BoxSize, BoxSize*2) select board.Tile(pos.Item1 + DefaultSize, pos.Item2 + DefaultSize * 2 - BoxSize)); queriesForBlocked.Add(from pos in box(BoxSize*2, BoxSize) select board.Tile(pos.Item1 , pos.Item2 + DefaultSize)); queriesForBlocked.Add(from pos in box(BoxSize*2, BoxSize) select board.Tile(pos.Item1 + DefaultSize * 2 - BoxSize, pos.Item2 + DefaultSize)); foreach (var query in queriesForBlocked) { foreach (var tile in query) tile.Block(); } // Select the tiles in the 3 x 3 area (area.X, area.Y) and create rules for them foreach (var area in box(SamuraiAreas, SamuraiAreas)) { var tilesInArea = from pos in box(BoxSize, BoxSize) select board.Tile(area.Item1 * BoxSize + pos.Item1, area.Item2 * BoxSize + pos.Item2); if (tilesInArea.First().IsBlocked) continue; board.CreateRule("Area " + area.Item1.ToString() + ", " + area.Item2.ToString(), tilesInArea); } // Select all rows and create columns for them var cols = from pos in box(board.Width, 1) select new { X = pos.Item1, Y = pos.Item2 }; var rows = from pos in box(1, board.Height) select new { X = pos.Item1, Y = pos.Item2 }; foreach (var posSet in Enumerable.Range(0, board.Width)) { board.CreateRule("Column Upper " + posSet, from pos in box(1, DefaultSize) select board.Tile(posSet, pos.Item2)); board.CreateRule("Column Lower " + posSet, from pos in box(1, DefaultSize) select board.Tile(posSet, pos.Item2 + DefaultSize + BoxSize)); board.CreateRule("Row Left " + posSet, from pos in box(DefaultSize, 1) select board.Tile(pos.Item1, posSet)); board.CreateRule("Row Right " + posSet, from pos in box(DefaultSize, 1) select board.Tile(pos.Item1 + DefaultSize + BoxSize, posSet)); if (posSet >= BoxSize*2 && posSet < BoxSize*2 + DefaultSize) { // Create rules for the middle sudoku board.CreateRule("Column Middle " + posSet, from pos in box(1, 9) select board.Tile(posSet, pos.Item2 + BoxSize*2)); board.CreateRule("Row Middle " + posSet, from pos in box(9, 1) select board.Tile(pos.Item1 + BoxSize*2, posSet)); } } return board; } public static SudokuBoard SizeAndBoxes(int width, int height, int boxCountX, int boxCountY) { SudokuBoard board = new SudokuBoard(width, height); board.AddBoxesCount(boxCountX, boxCountY); return board; } public static SudokuBoard ClassicWith3x3Boxes() { return SizeAndBoxes(DefaultSize, DefaultSize, DefaultSize / BoxSize, DefaultSize / BoxSize); } public static SudokuBoard ClassicWith3x3BoxesAndHyperRegions() { SudokuBoard board = ClassicWith3x3Boxes(); const int HyperSecond = HyperMargin + BoxSize + HyperMargin; // Create the four extra hyper regions board.CreateRule("HyperA", from pos in box(3, 3) select board.Tile(pos.Item1 + HyperMargin, pos.Item2 + HyperMargin)); board.CreateRule("HyperB", from pos in box(3, 3) select board.Tile(pos.Item1 + HyperSecond, pos.Item2 + HyperMargin)); board.CreateRule("HyperC", from pos in box(3, 3) select board.Tile(pos.Item1 + HyperMargin, pos.Item2 + HyperSecond)); board.CreateRule("HyperD", from pos in box(3, 3) select board.Tile(pos.Item1 + HyperSecond, pos.Item2 + HyperSecond)); return board; } public static SudokuBoard ClassicWithSpecialBoxes(string[] areas) { int sizeX = areas[0].Length; int sizeY = areas.Length; SudokuBoard board = new SudokuBoard(sizeX, sizeY); var joinedString = String.Join("", areas); var grouped = joinedString.Distinct(); // Loop through all the unique characters foreach (var ch in grouped) { // Select the rule tiles based on the index of the character var ruleTiles = from i in Enumerable.Range(0, joinedString.Length) where joinedString[i] == ch // filter out any non-matching characters select board.Tile(i % sizeX, i / sizeY); board.CreateRule("Area " + ch.ToString(), ruleTiles); } return board; } } Program: static class Program { [STAThread] static void Main() { SolveFail(); SolveClassic(); SolveSmall(); SolveExtraZones(); SolveHyper(); SolveSamurai(); SolveIncompleteClassic(); } private static void SolveFail() { SudokuBoard board = SudokuFactory.SizeAndBoxes(4, 4, 2, 2); board.AddRow("0003"); board.AddRow("0204"); // the 2 must be a 1 on this row to be solvable board.AddRow("1000"); board.AddRow("4000"); CompleteSolve(board); } private static void SolveExtraZones() { // http://en.wikipedia.org/wiki/File:Oceans_Hypersudoku18_Puzzle.svg SudokuBoard board = SudokuFactory.ClassicWith3x3BoxesAndHyperRegions(); board.AddRow(".......1."); board.AddRow("..2....34"); board.AddRow("....51..."); board.AddRow(".....65.."); board.AddRow(".7.3...8."); board.AddRow("..3......"); board.AddRow("....8...."); board.AddRow("58....9.."); board.AddRow("69......."); CompleteSolve(board); } private static void SolveSmall() { SudokuBoard board = SudokuFactory.SizeAndBoxes(4, 4, 2, 2); board.AddRow("0003"); board.AddRow("0004"); board.AddRow("1000"); board.AddRow("4000"); CompleteSolve(board); } private static void SolveHyper() { // http://en.wikipedia.org/wiki/File:A_nonomino_sudoku.svg string[] areas = new string[]{ "111233333", "111222333", "144442223", "114555522", "444456666", "775555688", "977766668", "999777888", "999997888" }; SudokuBoard board = SudokuFactory.ClassicWithSpecialBoxes(areas); board.AddRow("3.......4"); board.AddRow("..2.6.1.."); board.AddRow(".1.9.8.2."); board.AddRow("..5...6.."); board.AddRow(".2.....1."); board.AddRow("..9...8.."); board.AddRow(".8.3.4.6."); board.AddRow("..4.1.9.."); board.AddRow("5.......7"); CompleteSolve(board); } private static void SolveSamurai() { // http://www.freesamuraisudoku.com/1001HardSamuraiSudokus.aspx?puzzle=42 SudokuBoard board = SudokuFactory.Samurai(); board.AddRow("6..8..9..///.....38.."); board.AddRow("...79....///89..2.3.."); board.AddRow("..2..64.5///...1...7."); board.AddRow(".57.1.2..///..5....3."); board.AddRow(".....731.///.1.3..2.."); board.AddRow("...3...9.///.7..429.5"); board.AddRow("4..5..1...5....5....."); board.AddRow("8.1...7...8.2..768..."); board.AddRow(".......8.23...4...6.."); board.AddRow("//////.12.4..9.//////"); board.AddRow("//////......82.//////"); board.AddRow("//////.6.....1.//////"); board.AddRow(".4...1....76...36..9."); board.AddRow("2.....9..8..5.34...81"); board.AddRow(".5.873......9.8..23.."); board.AddRow("...2....9///.25.4...."); board.AddRow("..3.64...///31.8....."); board.AddRow("..75.8.12///...6.14.."); board.AddRow(".......2.///.31...9.."); board.AddRow("..17.....///..7......"); board.AddRow(".7.6...84///8...7..5."); CompleteSolve(board); } private static void SolveClassic() { var board = SudokuFactory.ClassicWith3x3Boxes(); board.AddRow("...84...9"); board.AddRow("..1.....5"); board.AddRow("8...2146."); board.AddRow("7.8....9."); board.AddRow("........."); board.AddRow(".5....3.1"); board.AddRow(".2491...7"); board.AddRow("9.....5.."); board.AddRow("3...84..."); CompleteSolve(board); } private static void SolveIncompleteClassic() { var board = SudokuFactory.ClassicWith3x3Boxes(); board.AddRow("...84...9"); board.AddRow("..1.....5"); board.AddRow("8...2.46."); // Removed a "1" on this line board.AddRow("7.8....9."); board.AddRow("........."); board.AddRow(".5....3.1"); board.AddRow(".2491...7"); board.AddRow("9.....5.."); board.AddRow("3...84..."); CompleteSolve(board); } private static void CompleteSolve(SudokuBoard board) { Console.WriteLine("Rules:"); board.OutputRules(); Console.WriteLine("Board:"); board.Output(); var solutions = board.Solve().ToList(); Console.WriteLine("Base Board Progress:"); board.Output(); Console.WriteLine("--"); Console.WriteLine("--"); Console.WriteLine("All " + solutions.Count + " solutions:"); var i = 1; foreach (var solution in solutions) { Console.WriteLine("----------------"); Console.WriteLine("Solution " + i++.ToString() + " / " + solutions.Count + ":"); solution.Output(); } } } [1]: https://i.sstatic.net/EQVTA.png [2]: https://i.sstatic.net/rjZiQ.png [3]: https://i.sstatic.net/KvsEr.png [4]: https://i.sstatic.net/1DIp9.png