Sudoku

Für die, die die Regeln noch nicht kennen: die Ziffern Eins bis Neun werden so verteilt, dass in jedem drei-mal-drei-Feld, in jeder Spalte und in jeder Zeile jede Ziffer exakt einmal vorkommt.

Anmerkung, im Unterschied zu kommerziellen Sudokus ist die Lösung nicht immer eindeutig. Wenn beispielsweise nur zwo Zahlen „frei“ sind bei vier „freien“ Feldern, dürften schätzungweise zwei Lösungen vorliegen. Dann führt „Raten“ nahezu zwangsläufig zur Lösung.


#! /usr/bin/env wish
package require Tcl 8.5
package require Tk

namespace import ::tcl::mathfunc::* ::tcl::mathop::*

proc blink {widget args} {
  lassign [info level 0] blink
  if {$widget eq "info"} then {
    #
    # info
    #
    set result {}
    foreach event [after info] {
      set info [after info $event]
      if {[lindex $info end] eq "timer" &&
          [lindex $info 0 0] eq $blink} then {
        lassign $info cmd
        lassign $cmd blink widget
        if {[winfo exists $widget]} then {
          set line [list $blink $widget]
          set opts [lrange $cmd 2 end]
          if {[winfo class $widget] in {Text Canvas}} then {
            lappend line -tag [dict get $opts -tag]
          } 
          lappend line -att [dict get $opts -att]
          lappend result $line
        }
      }
    }
    lsort $result
  } elseif {$widget eq "stop"} then {
    #
    # stop all after-events
    #
    foreach event [after info] {
      set info [after info $event]
      if {[lindex $info end] eq "timer" &&
          [lindex $info 0 0] eq $blink} then {
        after cancel $event
      }
    }
  } elseif {[winfo exists $widget]} then {
    if {$args eq "stop"} then {
      $blink $widget -action stop
    } else {
      set class [winfo class $widget]
      #
      # prepare
      #
      set item {
        -step 0
        -red0 0
        -green0 0
        -blue0 0
        -tag blink
        -red1 65535
        -green1 65535
        -blue1 65535
        -interval 25
        -action continue
      }
      #
      # set default target attributes
      #
      switch -exact -- $class {
        Text {
          dict set item -att -foreground
        }
        Canvas {
          dict set item -att -fill
        }
        default {
          dict set item -att -fg
        }
      }
      #
      # customize by command line
      #
      dict for {key val} $args {
        dict set item $key $val
      }
      #
      # translate named colors if given
      #
      if {[dict exists $item -color0]} then {
        lassign [winfo rgb . [dict get $item -color0]] r g b
        dict set item -red0 $r
        dict set item -green0 $g
        dict set item -blue0 $b
        dict unset item -color0
      }
      if {[dict exists $item -color1]} then {
        lassign [winfo rgb . [dict get $item -color1]] r g b
        dict set item -red1 $r
        dict set item -green1 $g
        dict set item -blue1 $b
        dict unset item -color1
      }
      #
      if {[dict get $item -action] eq "continue"} then {
        #
        # calculate increasement of color
        #
        dict incr item -step
        if {[dict get $item -step] >= 100} then {
          dict set item -step 0
        }
        set pi [expr {
          atan2(0,-1)
        }]
        set factor [expr {
          (cos($pi * 2 * [dict get $item -step] / 100) + 1) / 2
        }]
        #
        # generate hexadecimal color string
        #
        set rrrrggggbbbb #
        #
        set red0 [dict get $item -red0]
        set red1 [dict get $item -red1]
        set red [expr {$red0+int(($red1-$red0)*$factor)}]
        append rrrrggggbbbb [format %04x $red]
        #
        set green0 [dict get $item -green0]
        set green1 [dict get $item -green1]
        set green [expr {$green0+int(($green1-$green0)*$factor)}]
        append rrrrggggbbbb [format %04x $green]
        #
        set blue0 [dict get $item -blue0]
        set blue1 [dict get $item -blue1]
        set blue [expr {$blue0+int(($blue1-$blue0)*$factor)}]
        append rrrrggggbbbb [format %04x $blue]
        #
        set tag [dict get $item -tag]
        set att [dict get $item -att]
        switch -exact -- $class {
          Canvas {
            $widget itemconfigure $tag $att $rrrrggggbbbb
          }
          Text {
            $widget tag configure $tag $att $rrrrggggbbbb
          }
          default {
            $widget configure $att $rrrrggggbbbb
          }
        }
        #
        # repeat
        #
        set interval [dict get $item -interval]
        after $interval [list blink $widget {*}$item]
        #
      } else {
        #
        # stop blinking of $widget
        #
        foreach event [after info] {
          set info [after info $event]
          set line [lindex $info 0]
          lassign $line proc arg 
          if {$proc eq $blink && $arg eq $widget} then {
            after cancel $event
          }
        }
      }
    }
  }
}

bind Tk <Destroy> exit

proc aloud args {puts $args; uplevel 1 $args}

set lineColor goldenrod4
set warnColor {light blue}

apply {
  c {
    variable lineColor
    destroy $c
    pack [canvas $c -width 270 -height 305 -bg white]
    wm title . Sudoku
    wm resizable . no no
    set font {Helvetica -25 bold}
    set cursorColor grey
    for {set i 0} {$i < 9} {incr i} {
      for {set j 0} {$j < 9} {incr j} {
        $c create text [+ [* $i 30] 15] [+ [* $j 30] 15]\
          -tags [list nr $i/$j col$i row$j]\
          -font $font -text "" -fill grey
      }
    }
    $c create text 0 0 -font $font -fill $cursorColor -tags cursor
    $c bind cursor <1> [list tryCoords $c %x %y yes]
    $c bind cursor <ButtonRelease> [subst {
      blink stop
      $c itemconfigure nr -fill grey
      $c itemconfigure cursor -fill $cursorColor
      $c itemconfigure fixed -fill $lineColor
    }]
    for {set i 1} {$i < 9} {incr i} {
      set w [expr {$i % 3 ? 1 : 2}]
      $c create line [* $i 30] 0 [* $i 30] 270 -width $w -fill $lineColor
      $c create line 0 [* $i 30] 270 [* $i 30] -width $w -fill $lineColor
    }
    $c create line 0 270 270 270 -width 1 -fill $lineColor
    for {set i 1} {$i <= 9} {incr i} {
      $c create line [- [* $i 30] 5] 280 [- [* $i 30] 25] 300\
        -fill white -width 4 -tags "blink stroke $i"
      $c create text [- [* $i 30] 15] 290\
        -text $i -font $font -tags "try$i try" -fill grey
      $c bind try$i <1> [list setNrCursor $c $i]
    }
    $c raise cursor
  }
} .c

proc occurrences-of {nr canvas} {
  set result 0
  foreach item [$canvas find withtag nr] {
    if {[$canvas itemcget $item -text] eq $nr} then {
      incr result
    }
  }
  set result
}

proc echo args {puts $args}

proc setNr {canvas x y nr {byHand no}} {
  set before [$canvas itemcget $x/$y -text]
  if {$before ne ""} then {
    $canvas itemconfigure stroke&&$before -fill white
  }
  $canvas itemconfigure $x/$y -text $nr
  if {[string is integer -strict $nr]} then {
    if {[occurrences-of $nr $canvas] > 8} then {
      $canvas itemconfigure stroke&&$nr -fill grey
      setNrCursor $canvas ""
      $canvas addtag done withtag stroke&&$nr
    } else {
      $canvas itemconfigure stroke&&$nr -fill white
      $canvas dtag stroke&&$nr done
    }
  }
  if {$byHand && [llength [$canvas find withtag done]] > 8} then {
    update
    after 250 success
  }
}

proc success {} {
  tk busy hold .c -cursor ""
  .c raise blink
  # .c itemconfigure blink -width 4
  blink .c -color0 yellow -color1 grey
}

proc setNrCursor {canvas nr} {
  variable lineColor
  $canvas itemconfigure try -fill grey
  if {$nr eq ""} then {
    $canvas configure -cursor ""
    $canvas itemconfigure cursor -text ""
    $canvas bind cursor <Motion> ""
    bind $canvas <Leave> ""
    bind $canvas <Enter> ""
  } elseif {[$canvas itemcget cursor -text] eq $nr} then {
    setNrCursor $canvas ""
  } else {
    $canvas configure -cursor none
    bind $canvas <Motion> [list $canvas coords cursor %x %y]
    bind $canvas <Enter> [list $canvas itemconfigure cursor -text $nr]
    bind $canvas <Leave> [list $canvas itemconfigure cursor -text ""]
    after idle [list event generate $canvas <Enter>]
    $canvas itemconfigure try$nr -fill $lineColor
  }
}

proc tryCoords {canvas xCoord yCoord {byHand no}} {
  variable lineColor
  variable warnColor
  set x [expr {$xCoord / 30}]
  set y [expr {$yCoord / 30}]
  if {$y > 8} then {
    setNrCursor $canvas [expr {$x+1}]
    return
  }
  if {"fixed" in [$canvas gettags $x/$y]} then {
    setNrCursor $canvas [$canvas itemcget $x/$y -text]
    after idle [list event generate $canvas <Enter>]
    return
  }
  set nr [$canvas itemcget cursor -text]
  if {[$canvas itemcget $x/$y -text] eq $nr} then {
    setNr $canvas $x $y "" $byHand
    return
  } else {
    set occurrences [concat\
      [colOccurrence $canvas $x $nr]\
      [rowOccurrence $canvas $y $nr]\
      [squareOccurrence $canvas $x $y $nr]]
    if {$occurrences eq ""} then {
      setNr $canvas $x $y $nr $byHand
      return $nr
    }
    # error
    foreach {x y} $occurrences {
      # $canvas itemconfigure $x/$y -fill red
      lappend erraneous  $x/$y
    }
    $canvas itemconfigure cursor -fill red
    lappend erraneous cursor
    blink $canvas -tag [join $erraneous ||]\
      -interval 10 -color0 $lineColor -color1 white
  }
}

proc colOccurrence {canvas x nr} {
  set result {}
  foreach item [$canvas find withtag col$x] {
    set text [$canvas itemcget $item -text]
    if {$text eq $nr} then {
      set tags [$canvas gettags $item]
      set index [lsearch $tags row*]
      set tag [lindex $tags $index]
      lappend result $x [string index $tag end]
    }
  }
  set result
}

proc rowOccurrence {canvas y nr} {
  set result {}
  foreach item [$canvas find withtag row$y] {
    set text [$canvas itemcget $item -text]
    if {$text eq $nr} then {
      set tags [$canvas gettags $item]
      set index [lsearch $tags col*]
      set tag [lindex $tags $index]
      lappend result [string index $tag end] $y
    }
  }
  set result
}

proc squareOccurrence {canvas x y nr} {
  if {$x < 3} then {
    set xRange {0 1 2}
  } elseif {$x < 6} then {
    set xRange {3 4 5}
  } else {
    set xRange {6 7 8}
  }
  if {$y < 3} then {
    set yRange {0 1 2}
  } elseif {$y < 6} then {
    set yRange {3 4 5}
  } else {
    set yRange {6 7 8}
  }
  #
  set result {}
  foreach i $xRange {
    foreach j $yRange {
      set text [$canvas itemcget $i/$j -text]
      if {$text eq $nr} then {
        lappend result $i $j
      }
    }
  }
  set result
}

proc collides? {canvas x y nr} {
  expr {
    [llength\
        [concat\
          [colOccurrence $canvas $x $nr]\
          [rowOccurrence $canvas $y $nr]\
          [squareOccurrence $canvas $x $y $nr]]]
    ? yes
    : no
  }
}

proc inc9 _nr {
  upvar $_nr nr
  set nr [expr {int($nr) % 9 + 1}]
}

proc startRand9 {} {
  expr {1 + int(rand() * 9)}
}

proc nextField {x y} {
  incr x	
  if {$x > 8} then {
    set x 0
    incr y
  }
  list $x $y
}

proc take1from _l {
  upvar $_l l
  set i [expr {int(rand() * [llength $l])}]
  set result [lindex $l $i]
  set l [lreplace $l $i $i]
  set result
}

proc checkField {canvas {x 0} {y 0}} {
  if {$y > 8} then {
    return yes
  }
  lassign [nextField $x $y] x1 y1
  set l {1 2 3 4 5 6 7 8 9}
  while {[llength $l]} {
    set nr [take1from l]
    if {[collides? $canvas $x $y $nr]} then continue
    setNr $canvas $x $y $nr
    if {[checkField $canvas $x1 $y1]} then {
      return yes
    } else {
      setNr $canvas $x $y ""
    }
  }
  setNr $canvas $x $y ""
  return no
}

# debug

proc randomly? {} {
  expr {rand() > 0.5 ? yes : no}
}

apply {
  canvas {
    variable lineColor
    foreach i {0 1 2 3 4 5 6 7 8} {
      foreach j {0 1 2 3 4 5 6 7 8} {
        $canvas itemconfigure $i/$j -text ""
      }
    }
    checkField $canvas
    foreach i {0 1 2 3 4 5 6 7 8} {
      foreach j {0 1 2 3 4 5 6 7 8} {
        if {[randomly?]} then {
          $canvas addtag fixed withtag $i/$j
        } else {
          $canvas dtag $i/$j fixed
          $canvas itemconfigure $i/$j -text ""
        }
      }
    }
    $canvas itemconfigure fixed -fill $lineColor
    # $canvas itemconfigure stroke -fill white
    foreach nr {1 2 3 4 5 6 7 8 9} {
      if {[occurrences-of $nr $canvas] > 8} then {
        $canvas itemconfigure stroke&&$nr -fill grey
        $canvas addtag done withtag stroke&&$nr
      } else {
        $canvas itemconfigure stroke&&$nr -fill white
        $canvas dtag stroke&&$nr done
      }
    }
}} .c

11.3.2022