For this assignment, you will write a Sudoku solver. To do so, you will (1) use Scala collections extensively, (2) implement a backtracking search algorithm, and (3) implement constraint propagation.
We assume you know how to play Sudoku. If you don’t, you should play a few games by hand, before attempting this assignment.
The support code for this assignment is in the cmpsci220.hw.sudoku
package.
You should create a series of directories that look like this:
./sudoku |-- build.sbt `-- project `-- plugins.sbt `-- src |-- main | `-- scala | `-- your solution goes here `-- test `|-- scala `-- your tests goes here
Your build.sbt
file must have exactly these lines:
name := "sudoku"
scalaVersion := "2.11.2"
libraryDependencies += "edu.umass.cs" %% "cmpsci220" % "1.8"
libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.1" % "test"
parallelExecution in Test := false
fork in Test := true
The plugins.sbt
file must have exactly this line:
addSbtPlugin("edu.umass.cs" % "cmpsci220" % "2.2")
A Sudoku board is a 9-by-9 grid, where each cell is either blank or has a value in the range 1–9. For this assignment, we’ll use strings of length 81 to represent Sudoku boards, where each block of nine characters represents a successive row. For example, the following string:
val puzzle = "....8.3...6..7..84.3.5..2.9...1.54.8.........4.27.6...3.1..7.4.72..4..6...4.1...3"
Represents the following board:
| 8 |3
6 | 7 | 84
3 |5 |2 9
---+---+---
|1 5|4 8
| |
4 2|7 6|
---+---+---
3 1| 7| 4
72 | 4 | 6
4| 1 | 3
Here are some more examples:
".43.8.25.6.............1.949....4.7....6.8....1.2....382.5.............5.34.9.71."
"2...8.3...6..7..84.3.5..2.9...1.54.8.........4.27.6...3.1..7.4.72..4..6...4.1...3"
"..3.2.6..9..3.5..1..18.64....81.29..7.......8..67.82....26.95..8..2.3..9..5.1.3.."
"1..92....524.1...........7..5...81.2.........4.27...9..6...........3.945....71..6"
Your first task is to parse strings that represent Sudoku boards. You may assume that all strings represent solvable boards and that the string has exactly 81 characters. This part of the assignment should be trivial.
Solving Sudoku puzzles is much harder, but we’ll walk you through it.
Solving Sudoku puzzles is much harder, but we’ll walk you through it.
Backtracking search is a recursive algorithm that operates as follows. Given a a Sudoku board, B:
This approach will work in principle. But, in practice there are too many boards to search: there are 81 squares, and each square can hold 10 values (a digit or blank). Therefore, there are 10^81 possible boards, which exceeds the number of atoms in the observable universe.
To actually solve Sudoku problems, we need to combine backtracking search with constraint propogation. When you play Sudoku yourself, every time you fill a digit into a cell, you can eliminate that digit from several other cells. For example, if you fill 2 into the top-left corner of the board above, you can eliminate 2 from the first row, first column, and first box. i.e., there is no point even trying to place 2 in those spots, since the 2 in the corner constrains those cells.
We’ll augment backtracking search with constraint propagation, which implements this intuition. The key idea is to store not the value at a cell, but the set of values that may be placed in a cell. For example, on the empty board the values 1—9 may be placed at any cell:
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
------------------------------+-------------------------------+------------------------------
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
------------------------------+-------------------------------+------------------------------
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
123456789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
With this representation, when we place a value at a cell, we eliminate it from the other cells in the same row, column, and box (collectively known as the peers of a cell). For example, if we place 5 at the top-left corner of the empty board, we can elimiate 5 from the peers of the top-left corner:
5 1234 6789 1234 6789 | 1234 6789 1234 6789 1234 6789 | 1234 6789 1234 6789 1234 6789
1234 6789 1234 6789 1234 6789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
1234 6789 1234 6789 1234 6789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
------------------------------+-------------------------------+------------------------------
1234 6789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
1234 6789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
1234 6789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
------------------------------+-------------------------------+------------------------------
1234 6789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
1234 6789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
1234 6789 123456789 123456789 | 123456789 123456789 123456789 | 123456789 123456789 123456789
This procedure will significantly reduce the number of boards that need to be visited.
Your task is to implement the SolutionLike
and BoardLike
traits.
You can use the following template.
import cmpsci220.hw.sudoku._
object Solution extends SudokuLike {
type T = Board
def parse(str: String): Board = {
throw new UnsupportedOperationException("not implemented")
}
// You can use a Set instead of a List (or, any Iterable)
def peers(row: Int, col: Int): List[(Int, Int)] = {
throw new UnsupportedOperationException("not implemented")
}
}
// Top-left corner is (0,0). Bottom-right corner is (8,8).
// You don't have to have a field called available. Feel free to change it.
class Board(val available: Map[(Int, Int), List[Int]]) extends BoardLike[Board] {
def availableValuesAt(row: Int, col: Int): List[Int] = {
// Assumes that a missing value means all values are available. Feel
// free to change this.
available.getOrElse((row, col), 1.to(9).toList)
}
def valueAt(row: Int, col: Int): Option[Int] = {
throw new UnsupportedOperationException("not implemented")
}
def isSolved(): Boolean = {
throw new UnsupportedOperationException("not implemented")
}
def isUnsolvable(): Boolean = {
throw new UnsupportedOperationException("not implemented")
}
def place(row: Int, col: Int, value: Int): Board = {
require(availableValuesAt(row, col).contains(value))
throw new UnsupportedOperationException("not implemented")
}
// You can return any Iterable (e.g., Stream)
def nextStates(): List[Board] = {
if (isUnsolvable()) {
return List()
}
throw new UnsupportedOperationException("not implemented")
}
def solve(): Option[Board] = {
throw new UnsupportedOperationException("not implemented")
}
}
We recomend proceeding in this order and testing as you go along:
Implement Solution.peers
. peers(r, c)
produces the coordinates of all
cells in the same row as r
, same column as c
, and same block as (r,c)
.
Do not include (r, c)
in the set of its peers. i.e.,
peers(r,c).contains((r, c)) == false
Implement Solution.parse
You should assume that the input string is
matches the regular expression """(\.|[1-9]){81}""".r
and that
each block of nine characters represents a row (i.e., row-major order).
As the template suggests, you need to store the set of available values at
each cell instead of the value at the cell. You’ll need to define
the empty board as the map with all values available at each cell and
implement parse
using peers
as a helper function.
Implement Board.valueAt
. You should produce the digit stored at the
given row and column or None
if it is blank.
Implement Board.isSolved
and Board.isUnsolvable
. You may assume
that the constraints represented by available
are valid. Therefore, a board
is solved if every cell is constrained to exactly one value. Similarly,
a board is unsolvable if any cell is constrained to the empty set of values
(i.e., nothing can be placed in that cell).
Implement Board.place
. place(row, col, value)
produces a new board with
value
placed at (row, col)
of this
board. You may assume that
availableValuesAt(row, col).contains(value)
is true.
For the new board to be valid, you’ll have to:
value
from set of available values of each peer
(i.e., peers(row, col)
).Implement nextStates
, which returns the list of all boards that have
exactly one additional value placed on the board.
You should sort the returned list: ensure that boards with fewer available values occur earier in the list.
Implement solve
. If this.isSolution
is true, then return Some(this)
.
If not, iterate through the list of nextStates
, applying solve
to
board. Return the first solution that you find. If no solution is found,
return None
.
Your solver will not be able to solve arbitrary Sudoku boards. But, here are some boards that should work:
val fromCS121_1 = "85....4.1......67...21....3..85....7...982...3....15..5....43...37......2.9....58"
val fromCS121_2 = ".1.....2..3..9..1656..7...33.7..8..........89....6......6.254..9.5..1..7..3.....2"
val puz1 = ".43.8.25.6.............1.949....4.7....6.8....1.2....382.5.............5.34.9.71."
val puz2 = "2...8.3...6..7..84.3.5..2.9...1.54.8.........4.27.6...3.1..7.4.72..4..6...4.1...3"
There are several sources of Sudoku puzzles on the Web. Here are 50 purportedly easy puzzles:
You can use this terminal command to translate them into the format for this assignment:
curl -s http://norvig.com/easy50.txt | tr '\n' ' ' | tr '0' '.' | sed 's/ //' | sed 's/ ======== /\'$'\n/g'
In addition, I encourage you to share solvable puzzles with each other on Piazza.
Here is a trivial test suite that simply checks to see if you’ve defined
the Solution
object with the right type:
class TrivialTestSuite extends org.scalatest.FunSuite {
test("The solution object must be defined") {
val obj : cmpsci220.hw.sudoku.SudokuLike = Solution
}
}
You should place this test suite in src/test/scala/TrivialTestSuite.scala
.
If this test suite does not run as-is, you risk getting a zero.
For this assignment there will be two testing and review phases worth 5% of your grade each (i.e., 10% total).
Due: Nov 11, 23:59
Create a test suite called GradedTestSuite.scala
. In it, place 5–10
interesting test cases for the parse
, valueAt
, and peers
methods. Write
tests that try to cover the space of interesting inputs to these
methods.
Submit this file to Captain Teach. Once you’ve logged in,
click sudoku-solver-phase-one
to submit GradedTestSuite.scala
.
Due: Nov 14, 23:59
Extend GradedTestSuite.scala
, adding 5–10
interesting test cases for the isSolved
, isSolvable
, and place
methods.
Write tests that try to cover the space of interesting inputs to these methods.
Submit this file to Captain Teach. Once you’ve logged in, click sudoku-solver-
phase-two
to submit GradedTestSuite2.scala
.
You can login to Captain Teach using your @umass.edu
e-mail address. To
ensure you are logged into this account, first visit:
Once you have authenticated your account visit:
https://www.captain-teach.org/umass-cmpsci220/assignments/
After uploading your work, you will be given an opportunity to preview and
change it. Once you are satisfied with your submission, be sure to click
the Publish
button for your assignment.
Note: You will only be reviewing test cases. This means you should not submit any implementation details for review.
Use the submit
command within sbt
to create submission.tar.gz
. Upload
this file to Moodle.