dusthillresident (14 december 2023)
This is a solution finder for the Pentomino puzzle, with features allowing you to specify custom board layouts, and to remove specified pentominos from the pool of available pieces.
It uses bitwise operations on integers to keep track of which squares of the board are filled and to test if the pentomino pieces can fit in certain positions on the board, taking advantage of the feature of Tcl that allows integers to be arbitrarily big.
I managed to get it running reasonably fast. It's fastest at solving puzzles which are taller than they are wide. It struggles a bit with very wide puzzles. If you find it taking a long while, try using the 'rotate board' button to make your puzzle tall instead of wide.
It makes for a fun little toy and might be useful as a demonstration of various features of Tcl/Tk.
package require Tk tk appname "Pentomino Puzzle Solver" # ========================== # ==== Board management ==== # ========================== set board 0 set boardW 0 set boardH 0 set boardMask [expr [string cat "0b" [string repeat "1" [expr {21*21}]]]] set rowMask [expr 0b111111111111111111111] set dotMask [expr 0b100000000000000000000] proc makeBoard {w h} { global board boardMask boardW boardH rowMask set row [string cat [string repeat "0" $w] [string repeat "1" [expr {21-$w}]]] set board [expr [string cat "0b" \ [string repeat [string repeat "1" 21] [expr {21-$h}]] \ [string repeat $row $h] \ ]] set boardW $w set boardH $h set ::defaultwinw [expr {$boardW*24.0+4.0}] set ::defaultwinh [expr {$boardH*24.0+4.0}] set ::savedBoard $board $::solverCanvas configure -width $::defaultwinw -height $::defaultwinh } # ========================== # ==== Piece management ==== # ========================== proc _makePiece {a} { set a [lmap i $a { string cat $i [string repeat "0" [expr {21-[string length $i]}]] }] return [expr [join [join [list "0b" $a]] ""]] } proc rotatos {a} { set a [lmap i $a {split $i ""}] set w [llength [lindex $a 0]] set h [llength $a] set new [string repeat "{[string repeat 0\ $h]} " $w] for {set y 0} {$y<$h} {incr y} { for {set x 0} {$x<$w} {incr x} { lset new $x [expr {$h-1-$y}] [lindex $a $y $x] } } return [lmap i $new { join [lrange $i 0 end] "" }] } proc makePiece {a} { foreach j {1 2} { foreach i {1 2 3 4} { incr arr([_makePiece $a]) set a [rotatos $a] } set a [lmap i $a {string reverse $i}] } return [array names arr] } # ................................ # . Definition of all the pieces . # ................................ set pieces [list \ [makePiece { 100 100 111 }] \ \ [makePiece { 100 111 001 }] \ \ [makePiece { 1111 0010 }] \ \ [makePiece { 001 011 110 }] \ \ [makePiece { 010 010 111 }] \ \ [makePiece { 100 111 010 }] \ \ [makePiece { 1110 0011 }] \ \ [makePiece { 0001 1111 }] \ \ [makePiece { 11111 }] \ \ [makePiece { 110 111 }] \ \ [makePiece { 010 111 010 }] \ \ [makePiece { 11 01 11 }] \ \ ] # ============================================================ # ==== Conversion of piece data for canvas representation ==== # ============================================================ proc p {x y} { set x [expr {[expr $x]>>1}] set y [expr {[expr $y]>>1}] if {$x<0 || $y<0} {return 0} upvar p p global dotMask return [expr {$p & ($dotMask>>$x<<$y*21)}] } proc lrotate {l n} { set n [expr {$n % [llength $l]}] return [join [list [lrange $l $n end] [lrange $l 0 $n-1]]] } proc makePieceVertexList {p} { set x 0 set y 0 while {![p $x $y]} { incr y } set endy [expr {$y+1}] set dirx { 0 1 0 -1 } set diry { -1 0 1 0 } set direction 1 while 1 { lappend out [list $x $y] if {!$x && $y==$endy} break set l [list [p $x $y-1] [p $x+1 $y] [p $x $y+1] [p $x-1 $y]] lassign [lrotate $l $direction] up right down left set direction [expr {($direction + -($up && $left) + (!$up && $right)) & 3}] incr x [lindex $dirx $direction] incr y [lindex $diry $direction] } return [join $out] } set pieceColourList {green red blue violet orange khaki slate\ grey slate\ blue goldenrod cyan magenta orchid} set n 0 foreach piece $pieces { foreach variant $piece { set pieceVertices($variant) [makePieceVertexList $variant] set pieceColours($variant) [lindex $pieceColourList $n] } incr n } unset variant # ======================= # ==== Puzzle solver ==== # ======================= proc findFirstEmpty {} { global board boardW boardH dotMask rowMask set row $rowMask for {set y 0} {$y<$boardH} {incr y; set row [expr {$row<<21}]} { if {($board&$row)!=$row} { set dot [expr {$dotMask<<$y*21}] for {set x 0} {$x<$boardW} {incr x; set dot [expr {$dot>>1}]} { if {!($board & $dot)} { return [list $x $y $dot] } } } } error "findFirstEmpty is expected to be called only when we are certain that is an empty square on the board, this should not happen" } proc isPossibleFiller {m} { global fboard incr fboard $m set c 1 foreach i [list [expr {$m>>21}] [expr {$m<<21}] [expr {$m<<1}] [expr {$m>>1}]] { if {$i && !($fboard & $i)} { incr c [isPossibleFiller $i] } } return $c } proc isPossible {} { global fboard board boardMask lassign [findFirstEmpty] x y m set fboard $board return [expr {[isPossibleFiller $m]%5==0}] } set pieceIsAvailable {1 1 1 1 1 1 1 1 1 1 1 1} set solutionsFound 0 proc solve {} { global pieces board boardW boardH pieceIsAvailable boardMask solutionsFound solverUpdateCounter solverPaused solverAnimate solverAutoPause solverCancel if {$solverCancel} return set pieceNum 0 foreach piece $pieces { if {[lindex $pieceIsAvailable $pieceNum]} { lassign [findFirstEmpty] firstEmptyX y firstEmptyMask set holdBoard $board foreach variant $piece { for {set x [expr {$firstEmptyX>2?$firstEmptyX-3:0}]} {$x<=$firstEmptyX} {incr x} { set p [expr {$variant>>$x<<$y*21}] if {!($firstEmptyMask&$p) || $board & $p} continue set board [expr {$board|$p}] if {$board==$boardMask} { incr solutionsFound updateGui if {$solverAutoPause} { set solverPaused 1 . conf -cursor {} .solverScreen.topRow.step configure -state normal yield } set board $holdBoard continue } if {[isPossible]} { lset pieceIsAvailable $pieceNum 0 if {$solverPaused} { updateGui yield } elseif {$solverAnimate} { updateGui } else { # We want the gui to remain responsive even while the solver is running if {![set solverUpdateCounter [expr {$solverUpdateCounter+1 & 0xff}]]} update } solve if {$solverCancel} return lset pieceIsAvailable $pieceNum 1 } set board $holdBoard } } } incr pieceNum } } proc verifyPossible {} { global board fboard boardMask set holdBoard $board set fboard $board set result 1 while {$fboard != $boardMask} { if {[catch {lassign [findFirstEmpty] x y m}]} { set result 0 break } if {[isPossibleFiller $m]%5} {set result 0; break} set board $fboard } set board $holdBoard return $result } proc solverReset {} { for {set i 0} {$i<12} {incr i} { lset ::pieceIsAvailable $i [set ::pieceSwitch$i] } set ::solutionsFound 0 set ::solverCancel 0 set ::solverPaused $::solverStartPaused set ::solverCompleted 0 set ::board $::savedBoard } # ================== # ==== Main GUI ==== # ================== # ................. # . Solver screen . # ................. frame .solverScreen set selectedWidth 6 set selectedHeight 10 set solverPaused 0 set solverAutoPause 1 set solverAnimate 0 set solverStartPaused 0 set solverUpdateCounter 0 set solverCancel 0 set solverCompleted 0 pack [frame .solverScreen.topRow ] -fill x pack [checkbutton .solverScreen.topRow.pause -text "Paused" -variable solverPaused -command { .solverScreen.topRow.step configure -state [lindex {disabled normal} $solverPaused] if {!$solverPaused} { . conf -cursor watch solverCoroutine } else { . conf -cursor {} } } ] -side left pack [button .solverScreen.topRow.step -text "Step" -state disabled -command solverCoroutine] -side left pack [button .solverScreen.topRow.cancel -text "Cancel" -command { if {$::solverCompleted} { switchToOptions } else { set ::solverCancel 1 if {$::solverPaused} { solverCoroutine } } }] -side left pack [checkbutton .solverScreen.topRow.animate -text "Animate" -variable solverAnimate ] -side left pack [checkbutton .solverScreen.topRow.autoPause -text "Autopause" -variable solverAutoPause ] -side left pack [label .solverScreen.topRow.spacer] -side right -fill x -expand 1 pack [canvas .solverScreen.solver -background black] -fill both -expand 1 # .......................................... # . Options and board configuration screen . # .......................................... frame .optionScreen # top row pack [frame .optionScreen.topRow -borderwidth 1 -relief raised] -fill x pack [button .optionScreen.topRow.solve -text Solve] \ [label .optionScreen.topRow.l1 -text Width:] \ [ttk::spinbox .optionScreen.topRow.w -textvariable selectedWidth -width 3 -from 2 -to 20] \ [label .optionScreen.topRow.l2 -text Height:] \ [ttk::spinbox .optionScreen.topRow.h -textvariable selectedHeight -width 3 -from 2 -to 20] \ [label .optionScreen.topRow.spacesString -text "Extra spaces: "] \ [label .optionScreen.topRow.spacesInfo -width 3] -side left -padx 4 pack [label .optionScreen.topRow.spacer ] -side right -fill x -expand 1 # piece enable/disable row button .temp; set defaultBgColour [lindex [lsearch -inline -index 0 [.temp configure] -background] end]; destroy .temp pack [frame .optionScreen.pieceRow -borderwidth 1 -relief raised] -fill x for {set i 0} {$i<12} {incr i} { set f .optionScreen.pieceRow.p$i pack [frame $f] -side left lappend pieceIconCanvases $f.canv pack [canvas $f.canv -width 24 -height 24 -background $defaultBgColour] pack [checkbutton $f.check -variable pieceSwitch$i -command "incr ::extraSpaces \[expr \{\$pieceSwitch$i?-5:5\}\]; updateExtraSpacesDisplay" ] set pieceSwitch$i 1 } pack [label .optionScreen.pieceRow.spacer] -side right -fill x -expand 1 # last row, for misc options pack [frame .optionScreen.lastRow -borderwidth 1 -relief raised] -fill x pack [checkbutton .optionScreen.lastRow.animate -variable solverAnimate -text "Animate"] -side left pack [checkbutton .optionScreen.lastRow.autoPause -variable solverAutoPause -text "Autopause"] -side left pack [checkbutton .optionScreen.lastRow.startPaused -variable solverStartPaused -text "Start paused"] -side left pack [button .optionScreen.lastRow.flipWH -text "Rotate board" -command { set b $board set savedWH [list $selectedWidth $selectedHeight] set selectedWidth {} lassign $savedWH selectedHeight selectedWidth for {set x 0} {$x<$selectedWidth} {incr x} { for {set y 0} {$y<$selectedHeight} {incr y} { if {$b&($dotMask>>$y<<21*($selectedWidth-1-$x))} {.optionScreen.boardEditor.x$x,y$y invoke} } } }] -side left # .............................. # . Board configuration editor . # .............................. pack [frame .optionScreen.boardEditor ] -fill both -expand 1 proc updateExtraSpacesDisplay {} { .optionScreen.topRow.spacesInfo configure -text $::extraSpaces } proc flipBoardCell {x y} { set state [set ::btn$x,$y [expr {![set ::btn$x,$y]}]] set colour [lindex {black white} $state] .optionScreen.boardEditor.x$x,y$y configure -background $colour -activebackground $colour global extraSpaces board dotMask incr extraSpaces [expr {!$state-$state}] updateExtraSpacesDisplay set board [expr {$board ^ ($dotMask>>$x<<$y*21)}] } image create photo £ -width 12 -height 12 proc reconfigureBoardEditor {} { global boardW boardH boardSpaces extraSpaces foreach i [winfo children .optionScreen.boardEditor] {destroy $i} set boardSpaces [expr {$boardW*$boardH}] for {set y 0} {$y<$boardH} {incr y} { for {set x 0} {$x<$boardW} {incr x} { set btn .optionScreen.boardEditor.x$x,y$y set ::btn$x,$y 0 grid [button $btn -command [subst {flipBoardCell $x $y}] -image £ -background black -activebackground black] -column $x -row $y } } set extraSpaces [expr {$boardSpaces-5*[::tcl::mathop::+ {*}[set l {}; for {set i 0} {$i<12} {incr i} {lappend l [set ::pieceSwitch$i]}; set l]]}] updateExtraSpacesDisplay makeBoard $boardW $boardH after 11 fitWindow } foreach i {selectedWidth selectedHeight} { trace add variable $i {write} {apply {{args} { foreach i [list $::selectedWidth $::selectedHeight] {if {![string is integer -strict $i] || ($i<=0 || $i>20)} return} set ::boardW $::selectedWidth; set ::boardH $::selectedHeight; reconfigureBoardEditor }} } } # .......................................................................... # . 'Solve' button and management (starting/stopping) of the puzzle solver . # .......................................................................... .optionScreen.topRow.solve configure -command { if {$extraSpaces>0} { tk_messageBox -title "No possible solution" \ -message "Impossible board" \ -detail "This board configuration is unsolvable because there is more space than can be filled by the available pieces." } elseif {$extraSpaces%5} { tk_messageBox -title "No possible solution" \ -message "Impossible board" \ -detail "This board configuration is unsolvable because the amount of spaces is not divisble by 5,\nwhich means that it can't be filled by pentominoes." } elseif {![verifyPossible]} { tk_messageBox -title "No possible solution" \ -message "Impossible board" \ -detail "This board configuration is unsolvable because it contains spaces not divisable by 5, which means that it can't be filled by pentominoes." } elseif {$board == $boardMask} { tk_messageBox -title "Something happened" -message "Something happened" -detail "Something happened" } else { # Clear the solver canvas and switch context to the puzzle solver screen foreach i [$solverCanvas find all] { $solverCanvas dtag $i on; $solverCanvas move $i 65536 0 } switchToSolver # Start the solver set savedBoard $board solverReset updateCanvas eval [bind $solverCanvas <Configure>] .solverScreen.topRow.pause configure -state normal set solverPaused $solverStartPaused .solverScreen.topRow.step configure -state [lindex {disabled normal} $solverPaused] . conf -cursor [lindex {watch {}} $solverPaused] coroutine solverCoroutine apply {{} { solve if {$::solverCancel} { switchToOptions } else { tk_messageBox -title "Report" \ -message "Search completed" \ -detail "$::solutionsFound solutions were found." } .solverScreen.topRow.pause configure -state disabled .solverScreen.topRow.step configure -state disabled . conf -cursor {} solverReset set ::solverCompleted 1 set board $::savedBoard rename solverCoroutine {} }} } } proc fitWindow {} { set rw [winfo reqwidth .] set rh [winfo reqheight .] set w [winfo width .] set h [winfo height .] if {$rw>$w || $rh>$h} { wm geometry . [expr {max($rw,$w)}]x[expr {max($rh,$h)}] } } proc switchToSolver {} { pack forget .optionScreen pack .solverScreen -fill both -expand 1 after 11 fitWindow } proc switchToOptions {} { pack forget .solverScreen pack .optionScreen -fill both -expand 1 solverReset .solverScreen.topRow.pause configure -state normal .solverScreen.topRow.step configure -state disabled after 11 fitWindow } # =========================================== # ==== Solver canvas / graphical display ==== # =========================================== set defaultwinw 144.0 set defaultwinh 240.0 set solverCanvas .solverScreen.solver set linewidth 5 set n 0 foreach i $pieces { foreach v $i { set item [$solverCanvas create polygon $pieceVertices($v) \ -outline dark\ $pieceColours($v)\ -fill $pieceColours($v)\ -width $linewidth\ -tags [list p$v {pos 65536.0 0.0}]] } set c [lindex $pieceIconCanvases $n] set item [$c create polygon $pieceVertices($v) -outline dark\ $pieceColours($v) -fill $pieceColours($v) -width 1] $c scale all 0 0 3 3 lassign [$c bbox $item] x y x2 y2 set x [expr {12-($x2-$x)/2}] set y [expr {12-($y2-$y)/2}] $c moveto $item $x $y incr n } proc getPieceCPos {p} { set tag [lsearch -inline [$::solverCanvas gettags $p] pos*] return [lrange $tag 1 end] } proc setPieceCPos {p x y} { set tag [lsearch -inline [$::solverCanvas gettags $p] pos*] $::solverCanvas dtag $p $tag $::solverCanvas addtag [list pos $x $y] withtag $p } proc updateCanvas {} { global scf foreach i [$::solverCanvas find withtag on] { lassign [lsearch -inline [$::solverCanvas gettags $i] pos*] p x y $::solverCanvas moveto $i [expr {int($x*$scf+$scf/6.0)}] [expr {int($y*$scf+$scf/6.0)}] } } set scf 24.2 proc updateGui {} { global pieceVertices pieceColours scf solverCanvas set level 1 foreach i [$solverCanvas find all] { $solverCanvas dtag $i on $solverCanvas moveto $i 65536 0 } while 1 { if {[catch { upvar $level variant v x x y y set v }]} { updateCanvas update return } setPieceCPos p$v $x $y $solverCanvas addtag on withtag p$v incr level } } set scalingFactor 1.0 bind $solverCanvas <Configure> { set neww [winfo width $solverCanvas] set newh [winfo height $solverCanvas] if {$neww/$defaultwinw < $newh/$defaultwinh} { set a $defaultwinw set b $neww } else { set a $defaultwinh set b $newh } set factor [expr {double($b)/double($a)}] set undoPreviousScaling [expr {1.0/$scalingFactor}] $solverCanvas scale all 0 0 $undoPreviousScaling $undoPreviousScaling set scalingFactor [expr {$factor*12.0}] $solverCanvas scale all 0 0 $scalingFactor $scalingFactor set scf [expr {24.0*$factor}] foreach i [$solverCanvas find all] { $solverCanvas itemconfigure $i -width [expr {$scf/4.0}] } set winw [expr {double($neww)}] set winh [expr {double($newh)}] updateCanvas } # ============== # ==== Init ==== # ============== pack .optionScreen -fill both -expand 1 makeBoard 6 10 reconfigureBoardEditor