Version 1 of N-puzzle

Updated 2005-12-10 04:33:35

Keith Vetter 2005-12-10 : After writing The Classic 15 Puzzle I decided to write a generalized version that could both play and solve any size board.

I also added some color visualization and made the solving code much cleaner. The only tricky part was generating a solvable random board--the algorithm of counting inversions only seems to work for even sized boards. I never could figure that out, so instead I just take the solution position and move the tiles randomly 5000 times.


 ##+##########################################################################
 #
 # N-Puzzle.tcl -- Plays and solve the classic N-puzzle
 # by Keith Vetter, Dec 8 2005
 #
 # Solution algorithm taken from
 # http://www.javaonthebrain.com/java/puzz15/technical.html
 #

 package require Tk

 set S(n) 4
 set font Helvetica
 if {$tcl_platform(platform) eq "windows"} { set font {Comic Sans MS}}
 font create numfont -family $font -size 22 -weight bold
 ##+##########################################################################
 #
 # Init -- initializes everything to board size S(n)
 #
 proc Init {} {
    global S roundDisp roundDx

    set S(n1) [expr {$S(n) - 1}]                ;# Handy constants
    set S(n2) [expr {$S(n) * $S(n)}]
    set S(sz) [font measure numfont "15 "]      ;# Size of a cell
    set S(w) [expr {$S(n)*$S(sz) + 1}]          ;# Size of board

    set S(title) "N-Puzzle"
    set S(state) playing
    set S(soln) {}
    for {set i 1} {$i <= $S(n2)} {incr i} { lappend S(soln) [expr {$i%$S(n2)}]}

    # roundDisp are the offsets walking around a given cell
    set t [list -$S(n) [expr {-$S(n)+1}] 1 [expr {$S(n)+1}] $S(n) \
               [expr {$S(n)-1}] -1 [expr {-$S(n)-1}]]
    MakeArray roundDisp [concat $t $t $t $t]
    MakeArray roundDx {0 1 1 1 0 -1 -1 -1 0 1 1 1 0 -1 -1 -1 0 1 1 1 0 -1 -1 -1 0}
 }
 ##+##########################################################################
 #
 # Resize -- changes the size of the board
 #
 proc Resize {} {
    global S

    if {$S(state) eq "solving"} {               ;# Are we currently solving???
        set S(kill) 1
        set S(next) resize
        return
    }
    Init
    DoDisplay
    NewBoard
 }

 ##+##########################################################################
 #
 # DoDisplay -- puts up our display
 #
 proc DoDisplay {} {
    global S

    if {[winfo exists .c]} {
        .c delete all
        .c config -width $S(w) -height $S(w)
        return
    }

    bind all <Key-F2> {console show}
    wm title . $S(title)
    DoMenus
    canvas .c -width $S(w) -height $S(w) -highlightthickness 0 -bg gray75
    label .msg -textvariable S(msg) -bd 2 -relief ridge
    .msg configure  -font "[font actual [.msg cget -font]] -weight bold"
    pack .c -side top -padx 5 -pady 5
    pack .msg -side top -fill x
 }
 ##+##########################################################################
 #
 # DoMenus -- aren't installing menus really verbose and clunky?
 #
 proc DoMenus {} {
    option add *Menu.tearOff 0
    menu .menu
    . config -menu .menu

    menu .menu.game
    .menu add cascade -label "Game" -menu .menu.game
    .menu.game add command -label "New Board" -command NewBoard
    .menu.game add command -label "Solve" -command Solve

    .menu.game add separator
    set m .menu.game.size
    menu $m
    .menu.game add cascade -label "Board Size" -menu $m
    foreach n {2 3 4 5 6 7 8 9 10} {
        $m add radio -label "${n}x$n" -variable S(n) -value $n -command Resize
    }
    .menu.game add separator
    .menu.game add command -label "About" -command About
    .menu.game add command -label "Exit" -command exit
 }
 ##+##########################################################################
 #
 # Draws the board in array B
 #
 proc DrawNewBoard {} {
    global B

    .c delete all
    for {set row 0} {$row < $::S(n)} {incr row} {
        for {set col 0} {$col < $::S(n)} {incr col} {
            set r [TileRect $row $col]
            set xy [TileXY $row $col]
            set val $B($row,$col)
            set tag "tile$val"
            set tag2 "cell$val"

            if {$B($row,$col) == 0} {
                .c create rect $r -width 1 -fill gray75 -tag $tag
                continue
            }
            .c create rect $r -width 1 -fill white -tag [list tile $tag $tag2]
            .c create text $xy -text $val -font numfont -tag $tag
            .c bind $tag <1> [list Click $val]
        }
    }
 }
 ##+##########################################################################
 #
 # NewBoard -- creates a new board in B then draws it
 #
 proc NewBoard {} {
    global B S

    if {$S(state) eq "solving"} {
        set S(kill) 1
        set S(next) "new"
        return
    }

    ;#while {1} {
    ;#  set b [Shuffle $S(soln)]                ;# Pick a random board
    ;#  if {[Parity $b] == 0} break             ;# Is it legal?
    ;#}
    set b [ScrambleBoard]
    set idx -1
    for {set row 0} {$row < $S(n)} {incr row} {
        for {set col 0} {$col < $S(n)} {incr col} {
            set val [lindex $b [incr idx]]
            set B($row,$col) $val
            set B(r,$val) [list $row $col]
        }
    }
    DrawNewBoard
    set S(state) playing
    set S(msg) ""
 }
 ##+##########################################################################
 #
 # Creates a legal random board. To insure legality, it simulates
 # moving the tiles MAX times.
 #
 proc ScrambleBoard {{max 5000}} {
    array set DIRS {up {-1 0} down {1 0} left {0 -1} right {0 1}}
    set b $::S(soln)

    for {set i 0} {$i < $max} {incr i} {
        set idx0 [lsearch $b 0]                 ;# Find the hole
        set r0 [expr {$idx0 / $::S(n)}]
        set c0 [expr {$idx0 - $::S(n)*$r0}]

        while {1} {
            set dir [lindex {up down left right} [expr {int(rand()*4)}]]
            foreach {dr dc} $DIRS($dir) break

            set r1 [expr {$r0 + $dr}]
            set c1 [expr {$c0 + $dc}]
            if {$r1 >= 0 && $r1 < $::S(n) && $c1 >= 0 && $c1 < $::S(n)} break
        }
        set idx1 [expr {$r1*$::S(n) + $c1}]

        # Swap idx0 and idx1 in the board
        set temp [lindex $b $idx0]
        lset b $idx0 [lindex $b $idx1]
        lset b $idx1 $temp
    }
    return $b
 }
 ##+##########################################################################
 #
 # Parity -- returns the parity of the board. Parity is computed from the
 # sum of the number of inversions plus the row the hole is in (base 1)
 # Legal boards have parity of 0.
 #
 # Seems not to work for odd sized boards :(
 #
 proc Parity {{lboard {}}} {
    global B S

    if {$lboard eq {}} {                        ;# Turn board into a list
        set lboard {}
        for {set row 0} {$row < $S(n)} {incr row} {
            for {set col 0} {$col < $S(n)} {incr col} {
                lappend lboard $B($row,$col)
            }
        }
    }
    set hole [lsearch $lboard "0"]              ;# Where the hole is
    set lboard [lreplace $lboard $hole $hole]   ;# Ignore hole in inversions

    # Count inversions in this list
    set cnt 0
    for {set i 1} {$i < [llength $lboard]} {incr i} {
        set elem [lindex $lboard $i]
        for {set j 0} {$j < $i} {incr j} {
            if {[lindex $lboard $j] > $elem} {incr cnt}
        }
    }

    # Add in row the hole is in (base ($S(n)-1)%2)
    set row [expr {$hole / $S(n)}]
    incr cnt [expr {($hole / $S(n)) + $S(n1) % 2}]
    set parity [expr {$cnt % 2}]
    return $parity
 }
 ##+##########################################################################
 #
 # Shuffle -- shuffles a list
 #
 proc Shuffle {llist} {
    set len [llength $llist]
    set len2 $len
    for {set i 0} {$i < $len-1} {incr i} {
        set n [expr {int($i + $len2 * rand())}]
        incr len2 -1

        # Swap elements at i & n
        set temp [lindex $llist $i]
        lset llist $i [lindex $llist $n]
        lset llist $n $temp
    }
    return $llist
 }
 ##+##########################################################################
 #
 # Moves tiles in response to clicks on the board.
 #
 proc Click {val {force 0}} {
    global B

    if {! $force && $::S(state) ne "playing"} return
    foreach {row col} $B(r,$val) break
    foreach {hrow hcol} $B(r,0) break
    set dr [expr {$hrow-$row}]
    set dc [expr {$hcol-$col}]

    if {$dr != 0 && $dc != 0} return            ;# Diagonal move attempt
    if {$dr == 0 && $dc == 0} return            ;# NOP move attempt

    set adr [expr {$dr == 0 ? 0 : $dr/abs($dr)}];# Sign of dr
    set adc [expr {$dc == 0 ? 0 : $dc/abs($dc)}]
    set len [expr {abs($dr) + abs($dc)}]        ;# How many tiles too move

    for {set i 1} {$i <= $len} {incr i} {
        set r1 [expr {$hrow - $i * $adr}]
        set c1 [expr {$hcol - $i * $adc}]
        set val $B($r1,$c1)
        MoveTile $r1 $c1
        UpdateBoard $val 0
    }
    if {[IsSolved]} Victory
 }
 ##+##########################################################################
 #
 # MoveTile -- updates data structures for moving a tile
 #
 proc MoveTile {row col} {
    global B

    set val $B($row,$col)

    foreach {hrow hcol} $B(r,0) break
    set B($hrow,$hcol) $B($row,$col)            ;# Hole get tile's value
    set B($row,$col) 0                          ;# Tile is now hole
    set B(r,$val) [list $hrow $hcol]            ;# Reverse indices
    set B(r,0) [list $row $col]
 }
 ##+##########################################################################
 #
 # UpdateBoard -- updates board to reflect moved tile
 #
 proc UpdateBoard {val0 val1} {
    global B

    ;# NB. the tiles are ALREADY swapped in B
    foreach {x0 y0} [eval TileXY $B(r,$val0)] break
    foreach {x1 y1} [eval TileXY $B(r,$val1)] break

    set dx [expr {$x1 - $x0}]
    set dy [expr {$y1 - $y0}]
    .c move tile$val1 $dx $dy
    .c move tile$val0 [expr {-$dx}] [expr {-$dy}]
 }
 ##+##########################################################################
 #
 # Returns TRUE if B is solved
 #
 proc IsSolved {} {
    global B S

    set idx 0
    for {set row 0} {$row < $S(n)} {incr row} {
        for {set col 0} {$col < $S(n)} {incr col} {
            if {[incr idx] != $B($row,$col)} {  ;# Always fails for the hole
                return [expr {$idx == $S(n2)}]
            }
        }
    }
    return 0                                    ;# Should never get here
 }
 ##+##########################################################################
 #
 # Shows that you've won
 #
 proc Victory {} {
    .c itemconfig tile -fill magenta
    set ::S(state) solved
 }
 ##+##########################################################################
 #
 # Returns x,y of the center of tile at row,col
 #
 proc TileXY {row col} {
    set x [expr {$col * $::S(sz) + $::S(sz)/2}]
    set y [expr {$row * $::S(sz) + $::S(sz)/2}]
    return [list $x $y]
 }
 ##+##########################################################################
 #
 # Returns rectangle of tile at row,col
 #
 proc TileRect {row col} {
    set x0 [expr {$col * $::S(sz)}]
    set y0 [expr {$row * $::S(sz)}]
    set x1 [expr {$x0 + $::S(sz)}]
    set y1 [expr {$y0 + $::S(sz)}]
    return [list $x0 $y0 $x1 $y1]
 }
 proc About {} {
    set msg "N-Puzzle\nby Keith Vetter, December 2005\n\n"
    append msg "Lets you create and try to solve the\n"
    append msg "classic N-Puzzle. If you have trouble,\n"
    append msg "just press the Solve button to see it done."
    tk_messageBox -title "About N-Puzzle" -message $msg
 }

 ################################################################
 ################################################################
 #
 # Solution code below. Generalized from http://www.javaonthebrain.com
 #
 proc Solve {} {
    global S B MOVES HOLDER

    if {$S(state) eq "solving"} {               ;# Are we currently solving
        set S(kill) 1                           ;# Then stop
        set S(next) ""
        return
    }
    if {[IsSolved]} {                           ;# Already solved???
        set S(msg) "Already solved"
        Victory
        return
    }

    set S(state) solving
    set MOVES {}
    unset -nocomplain HOLDER
    for {set i 0} {$i < $S(n2)} {incr i} {
        foreach {row col} $B(r,$i) break
        set HOLDER([expr {$row*$S(n) + $col}]) $i
    }

    for {set row 0} {$row < $S(n)-2} {incr row} {
        SolveRow $row
    }
    SolveLast2Rows
    DoMoves
 }
 ##+##########################################################################
 #
 # SolveRow -- solves any row but the bottom 2. Columns 0 - n-2 are easy,
 # the last tow first go vertical then slip right in.
 #
 proc SolveRow {row} {
    global S HOLDER

    for {set col 0} {$col < $S(n)-2} {incr col} { ;# The easy column
        set cell [expr {$row * $S(n) + $col}]
        set who [expr {$cell + 1}]
        AddMessage msg "Putting $who in place"
        AddMessage start $who
        MoveTo $who $cell
        AddMessage done $who
    }
    set who [expr {$row * $S(n) + $S(n) - 1}]
    set who2 [expr {$who + 1}]
    set cell00 [expr {$row*$S(n) + $S(n) - 2}]
    set cell01 [expr {$row*$S(n) + $S(n) - 1}]
    set cell10 [expr {$row*$S(n) + 2*$S(n) - 2}]
    set cell11 [expr {$row*$S(n) + 2*$S(n) - 1}]

    if {$HOLDER($cell00) == $who && $HOLDER($cell01) == $who2} {
        AddMessage done $who
        AddMessage done $who2
        set HOLDER($cell00) -1
        set HOLDER($cell01) -1
        return
    }

    AddMessage msg "Putting $who,$who2 in place"
    AddMessage start $who
    AddMessage start $who2
    MoveTo $who $cell01
    set hpos [Locate 0]

    # Check where $who2 is
    if {$HOLDER($cell00) == $who2 && $hpos == $cell11} {
        AddMessage msg "Darn! $who2 needs a detour"
        MakeDetour {l u r d}
        MakeDetour {d l u r d l u r u l d r d}
    } elseif {$HOLDER($cell10) == $who2 && $hpos == $cell00} {
        AddMessage msg "Darn! $who2 needs a detour"
        MakeDetour {r d}
        MakeDetour {d l u r d l u r u l d r d}
    } elseif {$HOLDER($cell00) == $who2} {
        AddMessage msg "Darn! $who2 needs a detour"
        MoveTo $who2 $cell10
        MakeDetour {r d}
        MakeDetour {d l u r d l u r u l d r d}
    } else {
        MoveTo $who2 $cell11
    }

    # Now who is in cell01; who2 in cell11
    set HOLDER($cell01) $who                    ;# Unlock this piece
    set HOLDER($cell11) -1
    MoveTo $who $cell00
    AddMessage done $who
    set HOLDER($cell11) $who2                   ;# Unlock this piece
    MoveTo $who2 $cell01
    AddMessage done $who2
 }
 ##+##########################################################################
 #
 # SolveLast2Row -- like SolveRow but works horizontally
 #
 proc SolveLast2Rows {} {
    global S HOLDER

    set row [expr {$S(n) - 2}]

    for {set col 0} {$col < $S(n)-2} {incr col} {
        set who [expr {$row * $S(n) + $S(n) + $col + 1}]
        set who2 [expr {$row * $S(n) + $col + 1}]
        set cell00 [expr {$row * $S(n) + $col}]
        set cell01 [expr {$row * $S(n) + $col + 1}]
        set cell10 [expr {$row * $S(n) + $S(n) + $col}]
        set cell11 [expr {$row * $S(n) + $S(n) + $col + 1}]

        if {$HOLDER($cell10) == $who && $HOLDER($cell00) == $who2} {
            AddMessage done $who
            AddMessage done $who2
            set HOLDER($cell10) -1
            set HOLDER($cell00) -1
            continue
        }
        AddMessage msg "Putting $who,$who2 in place"
        AddMessage start $who
        AddMessage start $who2
        MoveTo $who $cell00
        set hpos [Locate 0]

        # Check where $who2 is
        if {$HOLDER($cell10) == $who2 && $hpos == $cell01} {
            AddMessage msg "Darn! $who2 needs a detour"
            MakeDetour {d l u r}
            MakeDetour {r d l u r d l u l d r u r}
        } elseif {$HOLDER($cell11) == $who2 && $hpos == $cell10} {
            AddMessage msg "Darn! $who2 needs a detour"
            MakeDetour {u r}
            MakeDetour {r d l u r d l u l d r u r}
        } elseif {$HOLDER($cell10) == $who2} {
            AddMessage msg "Darn! $who2 needs a detour"
            MoveTo $who2 $cell11
            MakeDetour {u r}
            MakeDetour {r d l u r d l u l d r u r}
        } else {
            MoveTo $who2 $cell01
        }

        set HOLDER($cell00) $who
        set HOLDER($cell01) -1
        MoveTo $who $cell10
        AddMessage done $who
        set HOLDER($cell01) $who2
        MoveTo $who2 $cell00
        AddMessage done $who2
    }

    # Spin the last 3 pieces into place
    set who00 [expr {$S(n2) - $S(n) - 1}]
    set cell00 [expr {$who00 - 1}]
    set who01 [expr {$S(n2) - $S(n)}]
    set cell01 [expr {$who01 - 1}]
    set who10 [expr {$S(n2) - 1}]
    set cell10 [expr {$who10 - 1}]

    AddMessage msg "Spinning last 3 pieces"
    AddMessage start $who00
    AddMessage start $who01
    AddMessage start $who10
    MoveTo $who00 $cell00
    MoveTo $who01 $cell01
    MoveTo $who10 $cell10
 }
 ##+##########################################################################
 #
 # MakeDetour -- follows a list of u,d,r&l
 #
 proc MakeDetour {dirs} {
    global S MOVES HOLDER
    array set DIRS [list "l" -1 "r" 1 "d" $S(n) "u" "-$S(n)"]

    set hpos [Locate 0]
    foreach dir $dirs {
        set to [expr {$hpos + $DIRS($dir)}]
        set HOLDER($hpos) $HOLDER($to)
        set HOLDER($to) 0
        set hpos $to
        lappend MOVES $to
    }
    return $MOVES
 }
 ##+##########################################################################
 #
 # AddMessage -- puts a message into move list to be displayed
 #
 proc AddMessage {type what} {
    lappend ::MOVES [list $type $what]
 }
 ##+##########################################################################
 #
 # MoveTo -- Moves "piece" to position "to"
 #
 proc MoveTo {piece to} {
    global HOLDER MOVES

    set ppath [GetPath $piece $to]
    set ppos [Locate $piece]
    set HOLDER($ppos) -1
    foreach tg $ppath {
        MoveHole $tg $ppos                      ;# Get the hole where we want it
        lappend MOVES $ppos                     ;# Move target into hole

        set HOLDER($ppos) 0                     ;# Update data structures
        set HOLDER($tg) -1
        set ppos $tg
    }
    return $MOVES
 }
 ##+##########################################################################
 #
 # GetPath -- gets path that "piece" will take to get to "to". How it completes
 # this path is somebody elses problem.
 #
 proc GetPath {piece to} {
    set ppath {}
    set hpos [Locate $piece]

    while {($hpos % $::S(n)) < ($to % $::S(n))} { ;# Go right if we need to
        lappend ppath [incr hpos]
    }
    while {($hpos % $::S(n)) > ($to % $::S(n))} { ;# Go left if we need to
        lappend ppath [incr hpos -1]
    }

    while {$hpos > $to} {                       ;# Get up if we need to
        lappend ppath [incr hpos -$::S(n)]
    }
    while {$hpos < $to} {                       ;# Get up if we need to
        lappend ppath [incr hpos $::S(n)]
    }
    return $ppath
 }
 ##+##########################################################################
 #
 # MoveHole -- the guts of the solution. Figures out how to get the hole to
 # the target position next to ppos without disturbing already solved tiles.
 #
 proc MoveHole {tg ppos} {
    global S HOLDER MOVES
    global roundDisp roundDx

    set hpos [Locate 0]                         ;# Find the hole
    foreach {hrow hcol} [list [expr {$hpos/$S(n)}] [expr {$hpos % $S(n)}]] break
    foreach {prow pcol} [list [expr {$ppos/$S(n)}] [expr {$ppos % $S(n)}]] break
    foreach {trow tcol} [list [expr {$tg / $S(n)}] [expr {$tg % $S(n)}]] break

    # Get in neighborhood of target
    while {abs($hcol - $pcol) > 1 || abs($hrow - $prow) > 1} {

        if {$hcol < $tcol && $HOLDER([expr {$hpos+1}]) > 0} {
            #puts "go right"
            set k [expr {$hpos + 1}]
            incr hcol
        } elseif {$hcol > $tcol && $HOLDER([expr {$hpos-1}]) > 0} {
            #puts "go left"
            set k [expr {$hpos - 1}]
            incr hcol -1
        } elseif {$hrow < $trow && $HOLDER([expr {$hpos+$S(n)}]) > 0} {
            #puts "go down"
            set k [expr {$hpos + $S(n)}]
            incr hrow
        } else {
            #puts "go up"
            set k [expr {$hpos - $S(n)}]
            incr hrow -1
        }

        lappend MOVES $k
        set HOLDER($hpos) $HOLDER($k)
        set HOLDER($k) 0
        set hpos $k
    }

    # Now we're 1 away from target. Find shortest path to target
    if {$hpos == $tg} return                    ;# Did we get lucky?

    # Locate where hpos is on perimeter of ppos
    for {set j 8} {$hpos != $ppos + $roundDisp($j)} {incr j} {}

    # Try going clockwise
    set posCount 0
    set k $j
    while {$ppos + $roundDisp($k) != $tg} {
        incr k
        set to [expr {$ppos + $roundDisp($k)}]

        if {$to >= 0 && $to < $S(n2) && $pcol+$roundDx($k) < $S(n) &&
            $pcol+$roundDx($k) >= 0 && $HOLDER($to) > 0} {
            incr posCount
        } else {
            incr posCount 50
        }
    }

    # Try going counter-clockwise
    set negCount 0
    set k $j
    while {$ppos+$roundDisp($k) != $tg} {
        incr k -1
        set to [expr {$ppos + $roundDisp($k)}]

        if {$to >= 0 && $to < $S(n2) && $pcol+$roundDx($k) < $S(n) &&
            $pcol+$roundDx($k) >= 0 && $HOLDER($to) > 0} {
            incr negCount
        } else {
            incr negCount 50
        }
    }

    # Pick optimal direction and do the moves
    set dir [expr {$posCount <= $negCount ? 1 : -1}]
    while {$hpos != $tg} {
        incr j $dir
        set k [expr {$ppos + $roundDisp($j)}]
        lappend MOVES $k
        set HOLDER($hpos) $HOLDER($k)
        set HOLDER($k) 0
        set hpos $k
    }
 }
 ##+##########################################################################
 #
 # Locate -- returns cell in which a given piece is located
 #
 proc Locate {num} {
    for {set i 0} {$num != $::HOLDER($i)} {incr i} {}
    return $i
 }
 ##+##########################################################################
 #
 # DoMoves -- walks our move list and visually does each move
 #
 proc DoMoves {} {
    global S B MOVES

    set S(kill) 0
    set S(next) ""
    set cnt 0
    foreach move $MOVES {
        if {$S(kill)} break
        if {[llength $move] > 1} {              ;# Not a move
            foreach {type what} $move break
            if {$type eq "done"} {
                .c itemconfig cell$what -fill green
            } elseif {$type eq "start"} {
                .c itemconfig cell$what -fill cyan
            } else {
                set S(msg) $what
            }
            continue
        }
        incr cnt
        foreach {row col} [list [expr {$move/$S(n)}] [expr {$move%$S(n)}]] break
        Click $B($row,$col) 1
        update
        after 200
    }
    set S(state) playing
    if {$S(kill)} {
        .c itemconfig tile -fill white
        set S(msg) "stopped"
        if {$S(next) eq "resize"} Resize
        if {$S(next) eq "new"} NewBoard
    } else {
        set MOVES {}
        set S(msg) "Done in $cnt move[expr {$cnt > 1 ? "s" : ""}]"
    }
 }
 ##+##########################################################################
 #
 # MakeArray -- turns a list into an array--easier access than lindex
 #
 proc MakeArray {_var values} {
    upvar $_var var
    set idx -1
    foreach v $values {
        set var([incr idx]) $v
    }
 }

 ################################################################
 ################################################################

 Init
 DoDisplay
 NewBoard
 return

Category Games