Skip to content

Commit

Permalink
Initial implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
esthervanenckevort committed Jun 3, 2020
0 parents commit ce4923f
Show file tree
Hide file tree
Showing 8 changed files with 449 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
.swiftpm/
28 changes: 28 additions & 0 deletions Package.swift
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"]),
]
)
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SudokuKit

A description of this package.
203 changes: 203 additions & 0 deletions Sources/SudokuKit/Board.swift
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
}
}
48 changes: 48 additions & 0 deletions Sources/SudokuKit/Solver.swift
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)
}
}
7 changes: 7 additions & 0 deletions Tests/LinuxMain.swift
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)
98 changes: 98 additions & 0 deletions Tests/SudokuKitTests/BoardTests.swift
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
}
}
}
}
Loading

0 comments on commit ce4923f

Please sign in to comment.