Version 6 of TkChallenger

Updated 2002-10-15 18:25:03

Keith Vetter - Here's a little tk program that lets you solve the Challenger puzzles that you often find syndicated in the daily paper [L1 ]. In this puzzle you must make each row, column and diagonal add up to a certain sum. This program aids in solving them by giving you running totals, showing illegal, correct and forced squares.

I wrote it with the intention that the user would enter the day's puzzle and then solve it. But I've also include a half-dozen or so puzzles from the last week to give you a taste.


KBK Lovely! Now to do an auto-solver, too, since Tcl is good at Solving cryptarithms and this sort of puzzle is amenable to similar techniques...

KPV Actually I already wrote one a long time ago, I'll see if I can dig it out. I used a brute force guessing solution, but if I remember correctly, if you are smart in which squares to guess first, you only have to guess about four squares before everything gets forced.

KPV You asked and you shall get. This version has a "solve" button. It generates code on the fly to do a brute-force search to solve the first two rows, after which everything is forced. This requires at most 6,561 (9^4) guesses.

http://mini.net/pub/tkchallenger.png


 ##+##########################################################################
 #
 # challenger.tcl
 #
 # Challenger -- helps solve the challenger math puzzle
 # by Keith Vetter
 #
 # Revisions:
 # KPV Oct 08, 2002 - initial revision
 # KPV Oct 14, 2002 - added auto solve
 #
 ##+##########################################################################
 #############################################################################

 # All the cells in the puzzle
 set state(cells) {      0 4
    1 0  1 1  1 2  1 3  1 4
    2 0  2 1  2 2  2 3  2 4
    3 0  3 1  3 2  3 3  3 4
    4 0  4 1  4 2  4 3  4 4
    5 0  5 1  5 2  5 3  5 4 }

 # Some puzzles to play with
 set puzzle(0) {16 . 1 . . 9 . . . 3 16 . . 2 . 17 1 . . . 9 11 18 12 10 12}
 set puzzle(1) {24 . . 3 . 14 . 9 . . 34 . . . 7 33 6 . . . 30 28 27 29 27 32}
 set puzzle(2) {18 . . 3 . 10 . 4 . . 20 . . . 1 10 5 . . . 20 12 13 17 18 12}
 set puzzle(3) {7 . 1 . . 8 . . . 1 9 . . 1 . 10 1 . . . 11 9 9 9 11 12}
 set puzzle(4) {23 . . . 5 23 6 . . . 23 . . 6 . 23 . 5 . . 23 25 19 25 23 23}
 set puzzle(5) {33 . 9 . . 28 . . . 6 30 . . 8 . 29 8 . . . 30 29 30 32 26 27}
 set puzzle(6) {35 . . 5 . 23 . 2 . . 21 . . . 9 20 8 . . . 31 20 24 20 31 17}
 set puzzle(7) {28 . 9 . . 26 . . 5 . 26 8 . . . 27 . . . 5 27 30 30 22 24 25}
 set puzzle(8) {15 5 . . . 16 . . . 3 6 . 3 . . 22 . . 4 . 15 21 12 7 19 9}
 set puzzle(9) {23 . . . 9 19 . 4 . . 11 7 . . . 15 . . 8 . 35 22 18 20 20 21}
 set puzzle(10) {9 2 . . .  8 . . 4 . 10 . . . 2 11 . 3 . . 15 10  6 14 14 16}
 set puzzle(11) {29 . . 8 . 30 2 . . . 22 . 6 . . 32 . . . 9 31 25 27 32 31 33}

 # The rows, columns and diagonals
 array set rows {
    1,4 {1,0 1,1 1,2 1,3}  2,4 {2,0 2,1 2,2 2,3}  3,4 {3,0 3,1 3,2 3,3}
    4,4 {4,0 4,1 4,2 4,3}  5,0 {1,0 2,0 3,0 4,0}  5,1 {1,1 2,1 3,1 4,1}
    5,2 {1,2 2,2 3,2 4,2}  5,3 {1,3 2,3 3,3 4,3}  5,4 {1,0 2,1 3,2 4,3}
    0,4 {4,0 3,1 2,2 1,3}
 }
 array set move {Up {-1 0} Down {1 0} Left {0 -1} Right {0 1}}

 set state(locked) 0
 set state(undo) {}
 set state(forced) {}
 set state(who) -1

 proc DoDisplay {} {
    wm title . "TkChallenger"
    DoMenus

    frame .play -bd 2 -relief raised -padx 30 -pady 0
    frame .play.tm -height 20
    grid .play.tm
    frame .bottom
    for {set row 0} {$row < 6} {incr row} {
        set cells {}
        if {$row == 0} {set cells "x x x x"}
        for {set col 0} {$col < 5} {incr col} {
            if {$row == 0} {set col 4}
            set tag ".e$row,$col"
            entry $tag -width 6 -textvariable ss($row,$col) -justify c \
                -disabledbackground lightblue -exportselection 0
            $tag config -disabledforeground [$tag cget -foreground]
            bind $tag <Key> [list MyKey %W %A %K]
            if {$row == 5 || $col == 4} {
                $tag config -bg cyan -disabledbackground cyan
            }
            lappend cells $tag
        }
        eval grid $cells -in .play -sticky we
    }
    label .msg -anchor w -bg [.play cget -bg] -textvariable state(msg)
    grid .msg - - - - -in .play -sticky ew -pady 5
    button .forced -text "Do Forced Moves" -command DoForced -takefocus 0
    button .undo -text Undo -command Undo -state disabled -takefocus 0
    button .solve -text Solve -command Solve -takefocus 0

    pack .play .bottom -side top -fill both -expand 1
    pack .solve .forced .undo -in .bottom -side left -pady 10 -expand 1

    array set ::b2m {.lock {.m.puzzle 4} .unlock {.m.puzzle 5} .undo {.m.edit 4}
        .erase {.m.edit 0} .eraseA {.m.edit 1} .forced {.m.edit 3}
        .solve {.m.puzzle 7}
    }
    focus .e1,0
    DoButtons
 }
 proc DoMenus {} {
    menu .m -tearoff 0
    . configure -menu .m                 ;# Attach menu to main window

    # Top level menu buttons
    .m add cascade -menu .m.puzzle -label "Puzzle" -underline 0
    .m add cascade -menu .m.edit   -label "Edit"   -underline 0
    .m add cascade -menu .m.help   -label "Help"   -underline 0

    menu .m.puzzle -tearoff 0
    .m.puzzle add command -label "Reset Puzzle" -under 0 \
        -command {PickPuzzle -1}
    .m.puzzle add command -label "Blank Puzzle" -under 0 -command {Erase 1}
    .m.puzzle add command -label "New Puzzle" -under 0 -command PickPuzzle
    .m.puzzle add separator
    .m.puzzle add command -label "Lock Puzzle" -under 0 -command Lock
    .m.puzzle add command -label "Unlock Puzzle" -under 0 -command Unlock
    .m.puzzle add separator
    .m.puzzle add command -label Solve -under 0 -command Solve
    .m.puzzle add separator
    .m.puzzle add command -label Exit -under 0 -command exit

    menu .m.edit -tearoff 0
    .m.edit add command -label "Erase" -under 0 -command {Erase 0}
    .m.edit add command -label "Erase All" -under 6 -command {Erase 1}
    .m.edit add separator
    .m.edit add command -label "Do Forced Moves" -under 0 -command DoForced
    .m.edit add command -label "Undo"            -under 0 -command Undo

    menu .m.help -tearoff 0
    .m.help add command -label Help  -under 0 -command Help
 }
 proc INFO {msg} { set ::state(msg) $msg ; update}

 proc PickPuzzle {{who ""}} {
    global state ss puzzle

    if {$who == ""} {                           ;# Pick one at random
        set names [array names puzzle]
        set len [llength $names]
        while {1} {
            set n [expr {int(rand() * $len)}]
            set who [lindex $names $n]
            if {$who != $state(who)} break
            if {$len == 1} break
        }
    } elseif {$who == -1} {
        set who $state(who)
    }
    if {! [info exists puzzle($who)]} {
        Erase 0
        return
    }
    Erase 1
    set state(who) $who

    foreach {row col} $state(cells) val $puzzle($who) {
        if {$val == "."} {set val {}}
        set ss($row,$col) $val
    }
    Lock
    INFO "Puzzle #$who"
 }
 # DoButtons -- set the buttons state depending on circumstances
 proc DoButtons {} {
    global state b2m

    array set s {1 normal 0 disabled}
    set ww [list .lock .unlock .erase .eraseA .solve .undo .forced]

    # Get into bb the states we want
    if {$state(locked)} { set bb {0 1 1 0 1} } { set bb {1 0 0 1 0} }
    lappend bb [expr {[llength $state(undo)]   > 0 ? 1 : 0}]
    lappend bb [expr {[llength $state(forced)] > 0 ? 1 : 0}]

    foreach w $ww b $bb {
        if {[winfo exists $w]} {                ;# Configure the button
            $w configure -state $s($b)
        }
        foreach {m e} $::b2m($w) {              ;# Configure the menu
            $m entryconfigure $e -state $s($b)
        }
    }
 }
 # Lock -- locks (by disabling) all cells w/ values in them
 proc Lock {} {
    global ss state

    foreach {row col} $state(cells) {
        set tag ".e$row,$col"
        if {$row == 5 || $col == 4} {
            append ss($row,$col) "/  "
        } elseif {$ss($row,$col) == ""} continue
        $tag config -state disabled
    }
    set state(locked) 1
    set state(undo) {}
    DoButtons
    SumRows
    set w [focus -lastfor .]

    catch {
        if {[$w cget -state] != "normal"} {
            event generate $w <Tab>
        }
    }
 }
 # Unlock -- unlocks (by enabling) all cells
 proc Unlock {} {
    global ss state

    foreach {row col} $state(cells) {
        set tag ".e$row,$col"
        $tag config -state normal

        if {$row == 5 || $col == 4} {
            regsub {/.*} $ss($row,$col) {} ss($row,$col)
        }
    }
    set state(locked) 0
    DoButtons
 }
 # Erase -- erases either all non-locked cells, or all cells
 proc Erase {all} {
    if {! $all && $::state(locked) == 0} return

    set undo {}
    foreach {row col} $::state(cells) {
        set tag ".e$row,$col"
        if {$all || [$tag cget -state] == "normal"} {
            set was [$tag get]
            lappend undo $tag "$row,$col" $was
            set ::ss($row,$col) ""
        }
    }
    SumRows
    lappend ::state(undo) $undo
    if {$all} {
        INFO ""
        Unlock
        set ::state(who) "user"
    }
    focus .e1,0
    DoButtons
 }
 proc GetPuzzle {} {
    global state ss

    set p ""
    foreach {row col} $state(cells) {
        set val $ss($row,$col)
        if {$val == ""} {set val .}
        regsub {/.*} $val "" val
        append p "$val "
    }
    return [string trim $p]
 }

 # SumRows -- the workhorse of our program. Sums up each row and
 # updates the running total (deficit actually) and does cell
 # configuring for bad, good and forced cells.
 proc SumRows {} {
    global state ss

    if {$::state(locked) == 0} { return 0}

    foreach {row col} $state(cells) {           ;# Put all cells back to white
        set tag ".e$row,$col"
        if {[$tag cget -state] == "normal"} { $tag config -bg white }
        if {$row == 5 || $col == 4} { $tag config -disabledbackground cyan }
    }

    _SumRows ss

    foreach cell $state(good) {
        .e$cell config -disabledbackground green
    }
    foreach cell $state(bad) {
        .e$cell config -disabledbackground red
    }
    foreach {cell value} $state(forced) {
        .e$cell config -background yellow
    }
    DoButtons
    if {[llength $state(good)] == 10} { return 1 } ;# Solved
    if {[llength $state(bad)] != 0} { return -1 } ;# Bad
    return 0
 }
 proc _SumRows {_SS} {
    global rows state
    upvar 1 $_SS SS


    set state(forced) {}
    set state(bad) {}
    set state(good) {}

    foreach scell [array names rows] {          ;# Loop on each row/col/diagonal

        set n [regexp {\s*([0-9]+)/?} $SS($scell) => max]
        if {! $n} continue
        set sum 0
        set missing {}
        foreach cell $rows($scell) {            ;# Each cell in row/col/diag
            set val $SS($cell)
            if {[string is integer -strict $val]} {
                set sum [expr {$sum + $val}]
            } else {
                lappend missing $cell
            }
        }
        # Show running deficit
        set SS($scell) [format "%2d/%2d" $max [expr {$max - $sum}]]
        set SS(d,$scell) [expr {$max - $sum}]

        # Figure out bad, good or forced cells stuff
        set num [llength $missing]
        if {$num == 0 && $sum == $max} {
            lappend state(good) $scell
        } elseif {$num == 0 || $sum > $max} {
            lappend state(bad) $scell
        } else {
            set delta [expr {1.0 * ($max - $sum) / $num}]
            if {$delta < 1 || $delta > 9} {
                lappend state(bad) $scell
            } elseif {$num == 1 || $delta == 1.0 || $delta == 9.0} {
                foreach who $missing {
                    lappend state(forced) $who [expr {int($delta)}]
                }
            }
        }
    }
 }
 proc IsSolved {} {
    return [expr {[llength $::state(good)] == 10 ? 1 : 0}]
 }
 # DoForced -- fills in the values for all forced cells
 proc DoForced {{repeat 0}} {
    global state ss
    set undo {}                                 ;# So we can undo this action

    while {[llength $state(forced)] > 0} {
        catch {unset done}
        foreach {cell val} $state(forced) {
            if {[info exists done($cell)]} continue
            lappend undo ".e$cell" $cell $ss($cell)
            set ss($cell) $val
            set done($cell) $val
        }
        lappend state(undo) $undo
        SumRows
        if {! $repeat} break
    }
 }
 proc _DoForced {_SS} {
    global state
    upvar 1 $_SS SS

    _SumRows SS
    while {[llength $state(forced)] > 0} {
        foreach {cell val} $state(forced) {
            set SS($cell) $val
        }
        _SumRows SS
        if {[llength $state(bad)] > 0} { return -1 }
    }
    if {[llength $state(good)] == 10} { return 1 } ;# Solved
    if {[llength $state(bad)] != 0} { return -1 } ;# Bad
    return 0
 }
 # MyKey -- handles all keystrokes for each cell
 proc MyKey {w char sym} {
    regexp {([0-9]),([0-9])} $w who row col
    set before [$w get]                         ;# For undo info

    switch -- $sym {
        "Tab" { return -code continue }
        "asterisk" { Undo }
        z { if {$char == "\x1A"} Undo }
        "space" { $w delete 0 end }
        "BackSpace" - "Delete" {
            set pos [$w index insert]
            $w delete [incr pos -1] end
        }
        "Home" - "End" {
            if {$sym == "End"} {set event <Shift-Tab>} {set event <Tab>}
            focus .e0,4 ; event generate .e0,4 $event
        }
        "Up" - "Down" - "Left" - "Right" {
            foreach {drow dcol} $::move($sym) break
            while {1} {
                incr row $drow ; incr col $dcol
                set ww ".e$row,$col"
                if {! [winfo exists $ww]} {return -code break}
                if {[$ww cget -state] != "normal"} continue
                focus $ww
                break
            }
        }
        default {
            if {! [string is integer -strict $char]} { return -code break }
            if {$row == 5 || $col == 4} {               ;# Sum cells
                set val [$w get]
                if {$val != ""} {
                    set char [expr {(($val * 10) + $char) % 100}]
                }
            } elseif {$char == "0"} {
                set char ""
            }
            $w delete 0 end
            $w insert 0 $char
            #$w selection range 0 end
            $w icursor end
            if {$row < 5 && $col < 4} {
                event generate $w <Tab>         ;# Move to next cell
            }
        }
    }
    set now [$w get]
    if {$before != $now} {lappend ::state(undo) [list $w $who $before]}
    SumRows
    return -code break
 }
 # Undo -- Undoes the last operation
 proc Undo {} {
    global state ss

    # Pop off the event to undo
    set item [lindex $state(undo) end]
    set state(undo) [lrange $state(undo) 0 end-1]

    foreach {w who was} $item {
        set ss($who) $was
        focus $w
    }
    SumRows
    DoButtons

 }
 proc Help {} {
    catch {destroy .help}
    toplevel .help
    wm title .help "TkChallenger Help"
    wm geom .help "+[expr {[winfo x .] + [winfo width .] + 10}]+[winfo y .]"

    text .help.t -relief raised -wrap word -width 70 -height 34
    .help.t config -padx 10 -pady 10
    button .help.dismiss -text Dismiss -command {destroy .help}

    pack .help.t -side top -expand 1 -fill both
    pack .help.dismiss -side bottom -expand 1 -pady 10

    set bold "[font actual [.help.t cget -font]] -weight bold"
    .help.t tag configure title -justify center -foreground red \
        -font "Times 20 bold"
    .help.t tag configure title2 -justify center -font "Times 12 bold"
    .help.t tag configure bullet -font $bold
    .help.t tag configure bn -lmargin1 15 -lmargin2 15
    .help.t tag configure bn2 -lmargin1 15 -lmargin2 20

    .help.t insert end "TkChallenger\n" title
    .help.t insert end "by Keith Vetter\n\n" title2

    set m "This program helps you solve the Challenger puzzles that you often "
    append m "see in the daily paper. I wrote this program because I'm "
    append m "horrible at solving these puzzles--this helps but I'm still bad."
    .help.t insert end $m n \n\n

    .help.t insert end "How to Play" bullet \n
    set m "Fill in each square with a number, 1-9. "
    append m "Horizontal square should add to the total on "
    append m "the right, vertical squares to the number on the "
    append m "bottom and the main diagonals to the number in "
    append m "the upper and lower right."
    .help.t insert end $m bn \n\n

    .help.t insert end "What the Different Colored Squares Mean" bullet \n
    set m "- Cyan squares show the value each row or column must add up "
    append m "to along with the amount still needed to reach that value.\n"
    append m "- Light blue squares are playing squares with known values.\n"
    append m "- Green squares show when a row or column is correct.\n"
    append m "- Red squares show when a row or column is in an illegal state.\n"
    append m "- Yellow squares show squares for which the value is forced."
    .help.t insert end $m bn2 \n\n

    .help.t insert end "Built in Puzzles" bullet \n
    set m "TkChallenger comes with about half-a-dozen built in puzzles, "
    append m "but is really designed for you to create your own. "
    append m "Unlocking the puzzle allows you to enter values into "
    append m "any square. Once you've entered the puzzle, lock it and "
    append m "solve away."
    .help.t insert end $m bn \n\n

    .help.t insert end "Auto Solve" bullet \n
    set m "TkChallenge can solve the puzzle for you. It does a brute "
    append m "force search trying all possibilities for the top two "
    append m "rows, after which the remaining squares are usually forced. "
    append m "This requires at most 6,561 (9^4) guesses for the typical "
    append m "published Challenger puzzle that has 4 squares already filled "
    append m "in."
    .help.t insert end $m bn \n\n

    .help.t config -state disabled
 }
 proc Solve {} {
    INFO "searching for solution..."
    DoForced                                    ;# Fill all forced cells
    set start [clock click -milliseconds]
    set code [GenerateCode]                     ;# This code will solve it
    eval $code
    foreach {solved cnt} [solvex] break

    set start [expr {([clock click -milliseconds] - $start)/1000.0}]
    set guesses "guess" ; if {$cnt != 1} {set guesses guesses}
    if {$solved} {
        INFO "Solved: $start sec and $cnt $guesses"
        set ::state(undo) {}
    } else {
        INFO "No solution: $start sec and $cnt $guesses"
    }   
    DoButtons
 }
 proc GenerateCode {} {
    set braces 0
    set code "proc solvex {} \{\n set cnt 0\n"
    append code " array set SS \[array get ::ss]\n\n"

    set indent 1
    foreach row {1 2} {
        foreach {b code2} [GenCodeRow $row [string repeat " " $indent]] {
            incr braces $b
            append code $code2 "\n"
            incr indent $b
        }
    }
    set ind [string repeat " " $indent]

    append code $ind "set save \[array get SS]\n"
    append code $ind "incr cnt\n"
    append code $ind "set n \[_DoForced SS]\n"
    append code $ind "if {\$n == 1} \{\n"
    append code $ind " array set ::ss \[array get SS]\n"
    append code $ind " SumRows\n"
    append code $ind " return \[list 1 \$cnt]\n"
    append code $ind "\}\n"
    append code $ind "array set SS \$save\n"
    append code [string repeat "\}\n" $braces]
    append code " return \[list 0 \$cnt]\n"
    append code "\}"

    return $code
 }
 proc GenCodeRow {row indent} {
    global ss

    set missing {}
    foreach col {0 1 2 3} {
        if {$ss($row,$col) == {}} { lappend missing "SS($row,$col)" }
    }
    set num [llength $missing]
    if {$num == 0} {
        return [list 0 "$indent; # complete row\n"]
    }

    set last [lindex $missing end]
    set code ""
    set code2 "set $last \[expr {$ss(d,$row,4)"
    for {set i 0} {$i < $num-1} {incr i} {
        set cell [lindex $missing $i]
        append code $indent
        append indent " "
        append code "for {set $cell 1} {\$$cell < 10} {incr $cell} \{\n"
        append code2 " - \$$cell"
    }
    append code $indent $code2 "}]\n"
    append code $indent "if {\$$last < 1 || \$$last > 9} continue\n"
    return [list [incr num -1] $code]
 }
 ################################################################
 ################################################################

 DoDisplay
 PickPuzzle

Category Games