-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ce4923f
Showing
8 changed files
with
449 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj | ||
xcuserdata/ | ||
.swiftpm/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// swift-tools-version:5.2 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "SudokuKit", | ||
products: [ | ||
// Products define the executables and libraries produced by a package, and make them visible to other packages. | ||
.library( | ||
name: "SudokuKit", | ||
targets: ["SudokuKit"]), | ||
], | ||
dependencies: [ | ||
// Dependencies declare other packages that this package depends on. | ||
// .package(url: /* package url */, from: "1.0.0"), | ||
], | ||
targets: [ | ||
// Targets are the basic building blocks of a package. A target can define a module or a test suite. | ||
// Targets can depend on other targets in this package, and on products in packages which this package depends on. | ||
.target( | ||
name: "SudokuKit", | ||
dependencies: []), | ||
.testTarget( | ||
name: "SudokuKitTests", | ||
dependencies: ["SudokuKit"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# SudokuKit | ||
|
||
A description of this package. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import Foundation | ||
|
||
public struct Board { | ||
private var board: [Int] | ||
private let boardWidth = 9 | ||
private let boardRange = 0..<81 | ||
private let squareWidth = 3 | ||
|
||
public func getSquare(for index: Int) -> [Int] { | ||
precondition((0..<81).contains(index), "Index must be in range 0..<81") | ||
let squareInRow = index % boardWidth / 3 | ||
let topLeftIndex = squareInRow * 3 + (index / 27) * 27 | ||
let offsets = [0, 1, 2, 9, 10, 11, 18, 19, 20] | ||
var elements = Array<Int>.init(repeating: 0, count: 9) | ||
for (index, offset) in offsets.enumerated() { | ||
elements[index] = board[topLeftIndex + offset] | ||
} | ||
return elements | ||
} | ||
|
||
public func getRow(for index: Int) -> [Int] { | ||
precondition((0..<81).contains(index), "Index must be in range 0..<81") | ||
var elements = Array<Int>.init(repeating: 0, count: 9) | ||
let row = index / boardWidth | ||
let startIndex = row * boardWidth | ||
for index in 0..<9 { | ||
elements[index] = board[startIndex + index] | ||
} | ||
return elements | ||
} | ||
|
||
public func getColumn(for index: Int) -> [Int] { | ||
precondition((0..<81).contains(index), "Index must be in range 0..<81") | ||
let startIndex = index % boardWidth | ||
var elements = Array<Int>.init(repeating: 0, count: boardWidth) | ||
for offset in 0..<9 { | ||
elements[offset] = board[startIndex + offset * boardWidth] | ||
} | ||
return elements | ||
} | ||
|
||
public subscript(index: Int) -> Int { | ||
get { | ||
board[index] | ||
} | ||
set(newValue) { | ||
precondition((0..<81).contains(index), "Index must be in range 0..<81") | ||
board[index] = newValue | ||
} | ||
} | ||
|
||
public init(board: [Int]) { | ||
precondition(board.count == 81, "Board must have exactly 81 elements.") | ||
self.board = board | ||
} | ||
|
||
public func isValidSolution() -> Bool { | ||
let numbers = Set([1, 2, 3, 4, 5, 6, 7, 8, 9]) | ||
for index in stride(from: 0, through: 80, by: 9) { | ||
let row = Set(getRow(for: index)) | ||
if numbers != row { | ||
return false | ||
} | ||
} | ||
for index in 0..<9 { | ||
let column = Set(getColumn(for: index)) | ||
if numbers != column { | ||
return false | ||
} | ||
} | ||
for index in [0, 3, 6, 27, 30, 33, 54, 57, 60] { | ||
let square = Set(getSquare(for: index)) | ||
if numbers != square { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
public init?() { | ||
guard let board = Board.resolve(board: Board.fill(), from: 0) else { | ||
return nil | ||
} | ||
self.board = board | ||
} | ||
|
||
static private func resolve(board: [Int], from position: Int) -> [Int]? { | ||
print("resolving \(position)") | ||
guard position < board.count else { | ||
return board | ||
} | ||
var result: [Int]? | ||
let seen = collectSeenNumbers(for: position, on: board) | ||
if seen.contains(board[position]) { | ||
print("\(position) swap") | ||
result = swapNumber(at: position, on: board, excluding: seen) | ||
print("\(position) swap \(result != nil ? "successful" : "failed")") | ||
} else { | ||
print("\(position) step") | ||
result = resolve(board: board, from: position + 1) | ||
if result != nil { | ||
print("\(position + 1) resolved") | ||
return result | ||
} else { | ||
print("\(position) force swap") | ||
result = swapNumber(at: position, on: board, excluding: seen) | ||
print("\(position) force swap \(result != nil ? "successful" : "failed")") | ||
} | ||
} | ||
print("\(position) done") | ||
if let result = result { | ||
print(Board(board: result)) | ||
} else { | ||
print("nil") | ||
} | ||
return result | ||
} | ||
|
||
static private func collectSeenNumbers(for position: Int, on board: [Int]) -> Set<Int> { | ||
var seen = Set<Int>() | ||
// Row | ||
let rowStart = (position / 9) * 9 | ||
for index in rowStart..<position { | ||
precondition(index < position, "Index must be smaller than the current position") | ||
seen.insert(board[index]) | ||
} | ||
// Column | ||
for index in stride(from: position % 9, to: position, by: 9) { | ||
precondition(index < position, "Index must be smaller than the current position") | ||
seen.insert(board[index]) | ||
} | ||
return seen | ||
} | ||
|
||
static private func swapNumber(at position: Int, on board: [Int], excluding seen: Set<Int>) -> [Int]? { | ||
var searchPos = position | ||
var result: [Int]? | ||
repeat { | ||
if searchPos % 3 == 2 { | ||
searchPos += 7 | ||
} else { | ||
searchPos += 1 | ||
} | ||
guard searchPos < board.count && Board.inSameSquare(position, searchPos) else { | ||
print("no candidates for swap of \(position)") | ||
print("\(Board(board: board))") | ||
print("Seen: \(seen)") | ||
return nil | ||
} | ||
if !seen.contains(board[searchPos]) { | ||
var newBoard = board | ||
newBoard.swapAt(position, searchPos) | ||
result = resolve(board: newBoard, from: position + 1) | ||
if let result = result { | ||
return result | ||
} | ||
} | ||
} while result == nil | ||
print("no candidates for swap of \(position)") | ||
print("\(Board(board: board))") | ||
return nil | ||
} | ||
|
||
static private func inSameSquare(_ first: Int, _ second: Int) -> Bool { | ||
let column1 = first % 9 / 3 | ||
let row1 = first / 27 | ||
let column2 = second % 9 / 3 | ||
let row2 = second / 27 | ||
return column1 == column2 && row1 == row2 | ||
} | ||
|
||
private static func fill() -> [Int] { | ||
var numbers = Array(stride(from: 1, through: 9, by: 1)) | ||
var board = Array(repeating: 0, count: 81) | ||
for index in 0..<81 { | ||
if index % 9 == 0 { | ||
numbers.shuffle() | ||
} | ||
let indexInBox = ((index / 3) % 3) * 9 + ((index % 27) / 9) * 3 + (index / 27) * 27 + (index % 3); | ||
board[indexInBox] = numbers[index % 9] | ||
} | ||
return board | ||
} | ||
} | ||
|
||
extension Board: CustomStringConvertible { | ||
public var description: String { | ||
var workingCopy = "" | ||
for index in 0..<81 { | ||
workingCopy += " \(board[index] )" | ||
if index % 9 == 2 || index % 9 == 5 { | ||
workingCopy += "|" | ||
} | ||
if index % 9 == 8 { | ||
workingCopy += "\n" | ||
} | ||
if index % 27 == 26 && index != 80 { | ||
workingCopy += "------+------+------\n" | ||
} | ||
} | ||
return workingCopy | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
public struct Solver { | ||
public init() { | ||
} | ||
|
||
public func solve(puzzle: Board) -> [Board] { | ||
return solve(startingFrom: 0, puzzle: puzzle) | ||
} | ||
|
||
private func solve(startingFrom index: Int, puzzle: Board) -> [Board] { | ||
precondition((0..<81).contains(index), "startingFrom index must be in range 0..<81") | ||
var boards = [Board]() | ||
var startIndex = index | ||
while puzzle[startIndex] != 0 { | ||
startIndex += 1 | ||
guard startIndex < 81 else { | ||
guard puzzle.isValidSolution() else { | ||
return [] | ||
} | ||
return [puzzle] | ||
} | ||
} | ||
|
||
for number in 1...9 { | ||
let results = place(number: number, at: startIndex, on: puzzle) | ||
boards.append(contentsOf: results) | ||
} | ||
return boards | ||
} | ||
|
||
private func place(number: Int, at index: Int, on board: Board) -> [Board] { | ||
guard !board.getSquare(for: index).contains(number) else { | ||
return [] | ||
} | ||
guard !board.getColumn(for: index).contains(number) else { | ||
return [] | ||
} | ||
guard !board.getRow(for: index).contains(number) else { | ||
return [] | ||
} | ||
var newBoard = board | ||
newBoard[index] = number | ||
|
||
if index == 80 { | ||
return [newBoard] | ||
} | ||
return solve(startingFrom: index + 1, puzzle: newBoard) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import XCTest | ||
|
||
import SudokuKitTests | ||
|
||
var tests = [XCTestCaseEntry]() | ||
tests += SudokuKitTests.allTests() | ||
XCTMain(tests) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import Foundation | ||
import XCTest | ||
import SudokuKit | ||
|
||
final class BoardTests: XCTestCase { | ||
|
||
var board: Board! | ||
override func setUp() { | ||
board = Board(board: [ | ||
0, 0, 8, 2, 0, 0, 9, 0, 3, | ||
3, 4, 2, 0, 9, 5, 0, 0, 7, | ||
1, 9, 7, 0, 0, 0, 0, 0, 4, | ||
0, 0, 5, 3, 1, 2, 4, 7, 9, | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
2, 0, 0, 0, 7, 4, 5, 0, 0, | ||
0, 2, 0, 0, 0, 1, 0, 0, 5, | ||
0, 7, 0, 0, 0, 6, 8, 9, 1, | ||
8, 0, 0, 4, 3, 0, 7, 0, 6 | ||
]) | ||
} | ||
func testGetSquare() { | ||
let square1 = [0, 0, 8, 3, 4, 2, 1, 9, 7] | ||
let square9 = [0, 0, 5, 8, 9, 1, 7, 0, 6] | ||
let square1Indices = [0, 1, 2, 9, 10, 11, 18, 19, 20] | ||
let square9Indices = [60, 61, 62, 69, 70, 71, 78, 79, 80] | ||
for index in square1Indices { | ||
let result = board.getSquare(for: index) | ||
XCTAssert(result == square1, "Result for index \(index) must be equal to square1.") | ||
} | ||
for index in square9Indices { | ||
let result = board.getSquare(for: index) | ||
XCTAssert(result == square9, "Result for index \(index) must be equal to square9.") | ||
} | ||
} | ||
|
||
func testGetRow() { | ||
let row1 = [0, 0, 8, 2, 0, 0, 9, 0, 3] | ||
let row9 = [8, 0, 0, 4, 3, 0, 7, 0, 6] | ||
for index in 0..<9 { | ||
let result = board.getRow(for: index) | ||
XCTAssert(result == row1, "Result for index \(index) must be equal to row1.") | ||
} | ||
for index in 72..<81 { | ||
let result = board.getRow(for: index) | ||
XCTAssert(result == row9, "Result for index \(index) must be equal to row9.") | ||
} | ||
} | ||
|
||
func testGetColumn() { | ||
let column1 = [0, 3, 1, 0, 0, 2, 0, 0, 8] | ||
let column9 = [3, 7, 4, 9, 0, 0, 5, 1, 6] | ||
for index in stride(from: 0, through: 72, by: 9) { | ||
let result = board.getColumn(for: index) | ||
XCTAssert(result == column1, "Result for index \(index) must be equalt to column1.") | ||
} | ||
for index in stride(from: 8, through: 80, by: 9) { | ||
let result = board.getColumn(for: index) | ||
XCTAssert(result == column9, "Result for index \(index) must be equal to column9.") | ||
} | ||
} | ||
|
||
func testSubscript() { | ||
let results = [ | ||
0, 0, 8, 2, 0, 0, 9, 0, 3, | ||
3, 4, 2, 0, 9, 5, 0, 0, 7, | ||
1, 9, 7, 0, 0, 0, 0, 0, 4, | ||
0, 0, 5, 3, 1, 2, 4, 7, 9, | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
2, 0, 0, 0, 7, 4, 5, 0, 0, | ||
0, 2, 0, 0, 0, 1, 0, 0, 5, | ||
0, 7, 0, 0, 0, 6, 8, 9, 1, | ||
8, 0, 0, 4, 3, 0, 7, 0, 6 | ||
] | ||
for index in 0..<81 { | ||
let value = board[index] | ||
XCTAssert(value == results[index], "Result for index \(index) must be equal to the value in the array for the same index.") | ||
} | ||
for index in 0..<81 { | ||
board[index] = 255 | ||
XCTAssert(board[index] == 255, "Board value must be successfully set to a different value.") | ||
} | ||
} | ||
|
||
func testGenerateBoard() { | ||
for iteration in 0..<10_000 { | ||
print("========= \(iteration) =========") | ||
guard let board = Board() else { | ||
XCTFail("Should have generated a solution.") | ||
return | ||
} | ||
if !board.isValidSolution() { | ||
print(board) | ||
XCTFail("Generated board should be a valid solution.") | ||
return | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.