Post on 24-Dec-2015
1
Teaching Programmingwith Sudoku
Bill Sanders for Axel T. SchreinerKiller Examples Workshop at OOPSLA’09
6
Verifier
Compare a solution to a puzzle.
array slicing (row, column, box)
arbitrary shapes (composition)
Very cool exercise for XSLT.
7
Model
Represent puzzle state, perform changes.
grid of cells holding digit or candidates
information hiding principles
cell interface, state classes
Good exercise to test from command line.
8
Solving assistant (1)
Show entered digits, candidates.
dynamically constructed GUI
reusable GUI parts, e.g. digit view vs. candidate view
9
Solving assistant (2)
Nice playground for design patterns.
MVC
several views observe one model
undo/redo pattern
factories to add features
10
Solver
Solve using heuristics and/or backtracking.
functional approach
iterators for row/column/box
composed iterator for context
brute-force backtracking
Perfect for a functional language.
View implements Observer
should know candidates to disallow nonsense.
also need to infer from singletons.
Model extends Observable
void set (row, col, digit)
int[] get (row, col)
undo() redo() ...
pruning algorithms, solver?
getset could tell row, column, boxbut after undo cell needs to ask row, col, box:
search: for (int digit = 1; digit <= dim; ++ digit) { for (int c = 0; c < board[row].length; ++ c) if (c != col && board[row][c].equals(digit)) continue search; for (int r = 0; r < board.length; ++ r) if (r != row && board[r][col].equals(digit)) continue search; int r = (row/boxDim)*boxDim, c = (col/boxDim)*boxDim; for (int i = 0; i < boxDim; ++ i) for (int j = 0; j < boxDim; ++ j) if ((r+i != row || c+j != col) && board[r+i][c+j].equals(digit)) continue search; canBe.set(digit); }
search: for (int digit = 1; digit <= dim; ++ digit) { for (int c = 0; c < board[row].length; ++ c) if (c != col && board[row][c].equals(digit)) continue search; for (int r = 0; r < board.length; ++ r) if (r != row && board[r][col].equals(digit)) continue search; int r = (row/boxDim)*boxDim, c = (col/boxDim)*boxDim; for (int i = 0; i < boxDim; ++ i) for (int j = 0; j < boxDim; ++ j) if ((r+i != row || c+j != col) && board[r+i][c+j].equals(digit)) continue search; canBe.set(digit); }
singles — optional
if get().length == 1could run search until "nothing changes"...
but how to keep the information?
and how to undo?
HBP*
model the digits on the board...
*Half-Baked Programming
interface Digit { /** true if digit is result of move. */ boolean equals (int digit); /** true if the digit in the position is set or inferred. */ boolean isKnown (); /** true if digit could be in position. */ boolean canBe (int digit); /** result of get. */ int[] digits (); /** remove single digit from candidates if possible, return false or isKnown. */ boolean prune (int digit); }
interface Digit { /** true if digit is result of move. */ boolean equals (int digit); /** true if the digit in the position is set or inferred. */ boolean isKnown (); /** true if digit could be in position. */ boolean canBe (int digit); /** result of get. */ int[] digits (); /** remove single digit from candidates if possible, return false or isKnown. */ boolean prune (int digit); }
HBP* *Half-Baked Programming
class Move implements Digit { /** position on board. */ final int row, col; /** digit in this position. */ final int[] digits;
Move (int row, int col, int digit) { ... }
boolean equals (int digit) { return digits[0] == digit; }
boolean isKnown () { return true; }
boolean canBe (int digit) { return false; }
int[] digits () { return digits; }
boolean prune (int digit) { return false; } }
class Move implements Digit { /** position on board. */ final int row, col; /** digit in this position. */ final int[] digits;
Move (int row, int col, int digit) { ... }
boolean equals (int digit) { return digits[0] == digit; }
boolean isKnown () { return true; }
boolean canBe (int digit) { return false; }
int[] digits () { return digits; }
boolean prune (int digit) { return false; } }
undo
set pushes each Move on the undo stack and clears the redo stack.
undo pops a Move, pushes it on the redo stack, and removes the Move from the board.
redo pops a Move and performs like set.
freeze discards both stacks, e.g., to mark a puzzle or the begin of backtracking.
HBP* *Half-Baked Programming
class Digits implements Digit { final int row, col; int[] digits; final BitSet canBe = new BitSet(dim+1);
Digits (int row, int col) { ... search ... }
boolean equals (int digit) { return false; }
boolean isKnown () { canBe.cardinality() == 1; }
boolean canBe (int digit) { return canBe.get(digit); }
int[] digits () { if (digits == null) { ... convert from canBe ... } return digits; }
boolean prune (int digit) { if (!canBe.get(digit)) return false; digits = null; canBe.clear(digit); return isKnown(); } }
class Digits implements Digit { final int row, col; int[] digits; final BitSet canBe = new BitSet(dim+1);
Digits (int row, int col) { ... search ... }
boolean equals (int digit) { return false; }
boolean isKnown () { canBe.cardinality() == 1; }
boolean canBe (int digit) { return canBe.get(digit); }
int[] digits () { if (digits == null) { ... convert from canBe ... } return digits; }
boolean prune (int digit) { if (!canBe.get(digit)) return false; digits = null; canBe.clear(digit); return isKnown(); } }
Problems
Inferring single digits can be donebut it is messy and looks impossible to extend
View receives update and uses get for state but get does not distinguish moves from inferred singles
Where did the design fail?
OOP interface Observer { /** user's move. */ void move (int row, int col, int digit); /** candidates. */ void ok (int row, int col, BitSet digits); /** change in undo/redo. */ void queues (int undos, int redos); }
interface Observer { /** user's move. */ void move (int row, int col, int digit); /** candidates. */ void ok (int row, int col, BitSet digits); /** change in undo/redo. */ void queues (int undos, int redos); }
class Model { void addObserver (Observer observer) { ... } void set (int row, int col, int digit) { ... move|ok ... } void undo () { ... queues ... } // ...
class Model { void addObserver (Observer observer) { ... } void set (int row, int col, int digit) { ... move|ok ... } void undo () { ... queues ... } // ...
class View implements Observer { // very passive... void move (int row, int col, int digit) { ... JLabel ... } void ok (int row, int col, BitSet digits) { ... JList ... }
class View implements Observer { // very passive... void move (int row, int col, int digit) { ... JLabel ... } void ok (int row, int col, BitSet digits) { ... JList ... }
OOP interface Digit { /** digit set in position. */ int digit (); /** candidates in position. */ BitSet digits (); /** true if single digit. */ boolean isKnown ();
/** prune context. */ void infer0 (); /** digit is no candidate! */ void infer0 (int digit); /** compute candidates. */
void infer1 ();
/** move is no candidate! */ void infer1 (BitSet digits);
interface Digit { /** digit set in position. */ int digit (); /** candidates in position. */ BitSet digits (); /** true if single digit. */ boolean isKnown ();
/** prune context. */ void infer0 (); /** digit is no candidate! */ void infer0 (int digit); /** compute candidates. */
void infer1 ();
/** move is no candidate! */ void infer1 (BitSet digits);
model move and candidates.
host inference algorithms.
OOP interface Digit { /** digit set in position. */ int digit (); /** candidates in position. */ BitSet digits (); /** true if single digit. */ boolean isKnown ();
/** prune context. */ void infer0 (); /** digit is no candidate! */ void infer0 (int digit);
/** compute candidates. */ void infer1 ();
/** move is no candidate! */ void infer1 (BitSet digits);
interface Digit { /** digit set in position. */ int digit (); /** candidates in position. */ BitSet digits (); /** true if single digit. */ boolean isKnown ();
/** prune context. */ void infer0 (); /** digit is no candidate! */ void infer0 (int digit);
/** compute candidates. */ void infer1 ();
/** move is no candidate! */ void infer1 (BitSet digits);
class Move ... {
return digit;
throw ...;
return true;
for (c: context) c.infer0(digit);
digits.clear(digit);
class Move ... {
return digit;
throw ...;
return true;
for (c: context) c.infer0(digit);
digits.clear(digit);
class Digits ... {
throw ...;
return digits;
return digits .cardinality() == 1;
digits.clear(digit); ok(row, col, digits);
for (c: context) c.infer1(newDgts); ok(row, col, newDgts);
class Digits ... {
throw ...;
return digits;
return digits .cardinality() == 1;
digits.clear(digit); ok(row, col, digits);
for (c: context) c.infer1(newDgts); ok(row, col, newDgts);
OOP interface Digit { /** digit set in position. */ int digit (); /** candidates in position. */ BitSet digits (); /** true if single digit. */ boolean isKnown ();
/** prune context. */ void infer0 (); /** digit is no candidate! */ void infer0 (int digit);
/** compute candidates. */ void infer1 ();
/** move is no candidate! */ void infer1 (BitSet digits);
interface Digit { /** digit set in position. */ int digit (); /** candidates in position. */ BitSet digits (); /** true if single digit. */ boolean isKnown ();
/** prune context. */ void infer0 (); /** digit is no candidate! */ void infer0 (int digit);
/** compute candidates. */ void infer1 ();
/** move is no candidate! */ void infer1 (BitSet digits);
Digits.single (Move.no-op)
/** if this is a singleton, tell the others. */ void single () { if (digits.cardinality() == 1) { Digit d; for (Loop i = doContext(row, col); i.hasNext(); ) if ((d = i.next()) != this) d.single(digits); } } /** recurse if this changes into a singleton. */ boolean single (BitSet digits) { if (!this.digits.intersects(digits)) return false; this.digits.andNot(digits); ok(row, col); single(); return true; }
/** if this is a singleton, tell the others. */ void single () { if (digits.cardinality() == 1) { Digit d; for (Loop i = doContext(row, col); i.hasNext(); ) if ((d = i.next()) != this) d.single(digits); } } /** recurse if this changes into a singleton. */ boolean single (BitSet digits) { if (!this.digits.intersects(digits)) return false; this.digits.andNot(digits); ok(row, col); single(); return true; }
row iterator
/** for (int c = 0; c < dim; ++ c) return board[row][c] */ Loop doRow (final int row) { return new Loop() { Loop copy () { return doRow(row); } Digit next () { if (!hasNext()) throw new NoSuchElementException(); return board[row][n++]; } };}
/** for (int c = 0; c < dim; ++ c) return board[row][c] */ Loop doRow (final int row) { return new Loop() { Loop copy () { return doRow(row); } Digit next () { if (!hasNext()) throw new NoSuchElementException(); return board[row][n++]; } };}
abstract class Loop { /** state of the loop. */ int n = 0; /** deep copy with n reset to zero. */ abstract Loop copy (); /** default: n < dim. */ boolean hasNext () { return n < dim; } /** next item in the loop. */ abstract Digit next () throws NoSuchElementException; }
abstract class Loop { /** state of the loop. */ int n = 0; /** deep copy with n reset to zero. */ abstract Loop copy (); /** default: n < dim. */ boolean hasNext () { return n < dim; } /** next item in the loop. */ abstract Digit next () throws NoSuchElementException; }
context iterator
Loop doContext (final int row, final int col) { final Loop[] loop = { doRow(row), doColumn(col), doBox(row, col) }; return new Loop() { Loop copy () { return doContext(row, col); } boolean hasNext () { return loop[n].hasNext(); } Digit next () { Digit result = loop[n].next(); if (!loop[n].hasNext() && n < loop.length-1) ++ n; return result; } }; }
Loop doContext (final int row, final int col) { final Loop[] loop = { doRow(row), doColumn(col), doBox(row, col) }; return new Loop() { Loop copy () { return doContext(row, col); } boolean hasNext () { return loop[n].hasNext(); } Digit next () { Digit result = loop[n].next(); if (!loop[n].hasNext() && n < loop.length-1) ++ n; return result; } }; }
Digits.unique (Move.no-op)
boolean unique () { if (digits.cardinality() <= 1) return false; Loop[] loop = {doRow(row), doColumn(col), doBox(row, col)}; for (int i = 0; i < loop.length; ++ i) { // next = digits minus row/column/box BitSet next = (BitSet)digits.clone(); Digit d; while (loop[i].hasNext()) if ((d = loop[i].next()) != this) d.unique(next); // unique digit left? if (next.cardinality() == 1 && next.intersects(digits)) { // ok.. turn into singleton and tell others digits = next; ok(row, col); single(); return true; } } } /** clear this.digits in the incoming digits. */ void unique (BitSet digits) { digits.andNot(this.digits); }
boolean unique () { if (digits.cardinality() <= 1) return false; Loop[] loop = {doRow(row), doColumn(col), doBox(row, col)}; for (int i = 0; i < loop.length; ++ i) { // next = digits minus row/column/box BitSet next = (BitSet)digits.clone(); Digit d; while (loop[i].hasNext()) if ((d = loop[i].next()) != this) d.unique(next); // unique digit left? if (next.cardinality() == 1 && next.intersects(digits)) { // ok.. turn into singleton and tell others digits = next; ok(row, col); single(); return true; } } } /** clear this.digits in the incoming digits. */ void unique (BitSet digits) { digits.andNot(this.digits); }
Digits.pair (Move.no-op)
boolean pair () { if (digits.cardinality() != 2) return false; boolean result = false; Digit that, d; Loop[] loop = { doRow(row), doColumn(col), doBox(row, col) }; for (int i = 0; i < loop.length; ++ i) while (loop[i].hasNext()) if ((that = loop[i].next()) != this && that.pair(digits)) for (Loop j = loop[i].copy(); j.hasNext(); ) if ((d = j.next()) != this && d != that) result |= d.single(digits); // prune return result; } /** true if this.digits and incoming digits are the same. */ boolean pair (BitSet digits) { return this.digits.equals(digits); }
boolean pair () { if (digits.cardinality() != 2) return false; boolean result = false; Digit that, d; Loop[] loop = { doRow(row), doColumn(col), doBox(row, col) }; for (int i = 0; i < loop.length; ++ i) while (loop[i].hasNext()) if ((that = loop[i].next()) != this && that.pair(digits)) for (Loop j = loop[i].copy(); j.hasNext(); ) if ((d = j.next()) != this && d != that) result |= d.single(digits); // prune return result; } /** true if this.digits and incoming digits are the same. */ boolean pair (BitSet digits) { return this.digits.equals(digits); }
OOP Lessons
information hiding.
if instanceof considered harmful.
distribute algorithm through messages.
divide and conquer.
check if existing classes really suffice.
35
Backtracking
Brute-force backtracker in Haskell, requires
solved: true if done
choices: possible new puzzles created from a given situation
solve puzzle | solved puzzle = Just puzzle | otherwise = case filter (/= Nothing) attempts of [] -> Nothing (x:xs) -> x where attempts = map solve (choices puzzle)
solve puzzle | solved puzzle = Just puzzle | otherwise = case filter (/= Nothing) attempts of [] -> Nothing (x:xs) -> x where attempts = map solve (choices puzzle)
36
Geometry
Puzzle is a list of 81 digits, zero if not known.
Geometry is described as lists of indices, using infinite lists for generation.
solved sudoku = 0 `notElem` sudoku
context n = row n ++ col n ++ box n row n = take 8 [x | x <- [n - n `mod` 9 ..], x /= n] col n = take 8 [x | x <- [n `mod` 9, n `mod` 9 + 9 ..], x /= n] box n = [x+y | x <- take 3 [row, row+9 ..], y <- take 3 [col ..], x+y /= n ] where row = n - n `mod` 27 -- starting row of box col = n `mod` 9 - n `mod` 3 -- starting column of box
solved sudoku = 0 `notElem` sudoku
context n = row n ++ col n ++ box n row n = take 8 [x | x <- [n - n `mod` 9 ..], x /= n] col n = take 8 [x | x <- [n `mod` 9, n `mod` 9 + 9 ..], x /= n] box n = [x+y | x <- take 3 [row, row+9 ..], y <- take 3 [col ..], x+y /= n ] where row = n - n `mod` 27 -- starting row of box col = n `mod` 9 - n `mod` 3 -- starting column of box
37
Candidates and moving
Candidates are digits not in the context of a cell — simply prune from all.
A move is a digit and an index — simply copy the array and replace the digit at the position.
candidates sudoku cell = [digit | digit <- [1..9], safe digit] where safe digit = digit `notElem` [sudoku!!x | x <- context cell]
move (position, choice) = map choose (zip sudoku [0..]) where choose (digit, index) | position == index = choice | otherwise = digit
candidates sudoku cell = [digit | digit <- [1..9], safe digit] where safe digit = digit `notElem` [sudoku!!x | x <- context cell]
move (position, choice) = map choose (zip sudoku [0..]) where choose (digit, index) | position == index = choice | otherwise = digit
38
Possibilities and choices
zero is the index of the first unknown cell.
Possible moves combine this index with each candidate digit for the cell.
New puzzles result by making every possible move in the situation.
Haskell computes by lazy evaluation.
moves = zip (repeat zero) (candidates sudoku zero) where zero = length $ takeWhile (0 /=) sudoku
choices sudoku = map move moves
moves = zip (repeat zero) (candidates sudoku zero) where zero = length $ takeWhile (0 /=) sudoku
choices sudoku = map move moves
39
Notation and thought
Extensive syntax gets in the way.
Boilerplate clogs the mind.
Structures should be light-weight.
40
The references
The extended abstract references a paper and a number of assignments and solutions.
http://www.cs.rit.edu/~ats/papers/sudoku2/sudoku2.pdf
C# assignments and solutions for Squiggly Sudoku are at
http://www.cs.rit.edu/~ats/cs-2009-1/