1 teaching programming with sudoku bill sanders for axel t. schreiner killer examples workshop at...

40
1 Teaching Programming with Sudoku Bill Sanders for Axel T. Schreiner Killer Examples Workshop at OOPSLA’09

Upload: edward-wood

Post on 24-Dec-2015

214 views

Category:

Documents


0 download

TRANSCRIPT

1

Teaching Programmingwith Sudoku

Bill Sanders for Axel T. SchreinerKiller Examples Workshop at OOPSLA’09

2

The problem

The exercise yard

The path to hell...

The functional approach

The references

3

The problem

Each row, column, and box must contain all n digits.

3

4

The problem

Each row, column, and box must contain all n digits.

4

5

The exercise yard

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.

11

The path to hell...

View implements Observer

View implements Observer

View implements Observer

should know candidates to disallow nonsense.

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.

34

The functional approach

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/