Puzzle Blocks

Keith Vetter 2003-05-12 : drag, rotate and drop blocks to assemble a bird or a boat.

escargo 12 May 2003 - Wonderful fun. However, I noticed that only particular pieces that matched seemed to make noise and change color when they were put in the right place. Shouldn't equivalent pieces be treated the same way? Or are no two pieces geometrically identical?

Also, on the bird puzzle, when I put the piece that goes for the right side of the bird's head in place, the bottom line has a step in it, as if it were not a straight line. Is that the way it's supposed to be?

KPV - Yes, you're right--identical pieces should be able to go into any appropriate spot. It's on the to-do list but to be honest it probably won't get done. As to the step in some lines, it's round-off error. The pieces start off very small, get scaled up and possibly rotated numerous times. After a while, the round off error accumulates giving you the unwanted stair step.

See also Tangram


https://www.tcl-lang.org/starkits/puzzleblocks.jpg


HJG Added "Done"-Message when puzzle is completed. The website seems to have been "reorganized", the puzzle is now at http://invention.smithsonian.org/centerpieces/iap/playhouse_puzzle.html


uniquename 2013aug01

Here is an image of the GUI starting with the pieces for the boat puzzle. The bird is above.

vetter_PuzzleBlocks_boat_screenshot_635x550.jpg


Jeff Smith 2020-09-05 : Below is an online demo using CloudTk. This demo runs "Puzzle Blocks" in an Alpine Linux Docker Container. It is a 27.4MB image which is made up of Alpine Linux + tclkit + Puzzle-Blocks.kit + libx11 + libxft + fontconfig + ttf-linux-libertine. It is run under a user account in the Container. The Container is restrictive with permissions for "Other" removed for "execute" and "read" for certain directories.


##+##########################################################################
#
# Puzzle Blocks
# by Keith Vetter, May 9, 2003
# see http://invention.smithsonian.org/centerpieces/iap/playhouse_puzzle.html
#
# TODO
#  allow identical pieces to go into any identical spot

package require Tk
set S(title) "Puzzle Blocks"
set S(msg) ""
set S(snap) 10                                  ;# "Close enough" distance

proc DoDisplay {} {
    wm title . $::S(title)
    pack [frame .ctrl -relief ridge -bd 2 -padx 5 -pady 5] \
-side right -fill both -ipady 5
    pack [frame .screen -bd 2 -relief raised] -side top -fill both -expand 1

    canvas .c -relief raised -borderwidth 0 -height 500 -width 500
    button .bird -text "Bird Puzzle" -command {GoPuzzle Bird} -bd 4
    .bird configure  -font "[font actual [.bird cget -font]] -weight bold"
    option add *font [.bird cget -font]
    button .boat  -text "Boat Puzzle" -command {GoPuzzle Boat} -bd 4
    button .about -text About -command [list tk_messageBox -title About -message "$::S(title) \
by Keith Vetter, May 2003"]
    label .msg -textvariable S(msg) -bd 2 -bg white -relief ridge

    pack .msg   -in .screen -side bottom -fill both
    pack .c     -in .screen -side top    -fill both -expand 1
    grid .bird  -in .ctrl -sticky ew -row 0
    grid .boat  -in .ctrl -sticky ew
    grid rowconfigure .ctrl 50 -weight 1
    grid .about -in .ctrl -row 100 -sticky ew

    bind all <Alt-c>     {console show}
    bind .c  <Configure> {ReCenter %W %h %w}
    update
}
proc ReCenter {W h w} {                         ;# Called by configure event
    set h2 [expr {$h / 2}] ; set w2 [expr {$w / 2}]
    $W config -scrollregion [list -$w2 -$h2 $w2 $h2]
}
proc RotateItem {id Oxy angle} {
    foreach {Ox Oy} $Oxy break
    set rangle [expr {$angle * atan(1) * 4 / 180.0}] ;# Radians

    set xy {}
    foreach {x y} [.c coords $id] {
        # rotates vector (Ox,Oy)->(x,y) by angle degrees clockwise
        set x  [expr {$x - $Ox}]                ;# Shift to origin
        set y  [expr {$y - $Oy}]
        set xx [expr {$x * cos($rangle) - $y * sin($rangle)}] ;# Rotate
        set yy [expr {$x * sin($rangle) + $y * cos($rangle)}]
        set xx [expr {$xx + $Ox}]               ;# Shift back
        set yy [expr {$yy + $Oy}]
        lappend xy $xx $yy
    }
    .c coords $id $xy
    set ::C(rotate,$id) [expr {($::C(rotate,$id)+$angle) % 360}]
}
proc MoveItem {who x y} {
    foreach {cx cy} [Centroid $who] break
    .c move $who [expr {$x - $cx}] [expr {$y - $cy}]
}
proc GoPuzzle {who} {
    set ::S(msg) "Drag and drop the pieces; right click to rotate"
    .c delete all
    $who
    DrawBoard
    DrawBlocks
}
proc DrawBlocks {} {
    global C S Y

    catch {unset Y}
    foreach {x0 y0 x1 y1} [.c cget -scrollregion] break
    set maxx [expr {$x1 - 30}]
    set maxy [expr {$y1 - 30}]
    foreach b [array names C coords,*] {
        foreach {_ who} [split $b ,] break
        .c create poly $C($b) -tag $who -fill yellow -outline black
        set C(rotate,$who) 0
        set Y($who) 1
        .c scale $who 0 0 $C(scale) $C(scale)
        .c bind $who <Button-1>        [list Mouse1 $who %x %y 0]
        .c bind $who <B1-Motion>       [list Mouse1 $who %x %y 1]
        .c bind $who <ButtonRelease-1> [list Mouse1 $who %x %y 2]
        .c bind $who <Button-3>        [list Mouse3 $who]

        MoveItem   $who [Random -$maxx $maxx] [Random -$maxy $maxy]
        RotateItem $who [Centroid $who] [expr {int(rand()*8)*45}]
    }
}
proc DrawBoard {} {
    .c create poly $::C(board) -tag board -outline black -fill blue4 -dash 1
    .c scale board 0 0 $::C(scale) $::C(scale)
    MoveItem board 0 0
    .c lower board
    set ::C(board2) [.c coords board]
}
proc Mouse1 {who x y what} {
    global S C Y

    set x [.c canvasx $x]
    set y [.c canvasy $y]

    if {$what == 0} {                           ;# Button down
.c itemconfig $who -width 3 -fill yellow
set Y($who) 1                           ;# Mark as out of position
.c raise $who
    } elseif {$what == 2} {                     ;# Button up
.c itemconfig $who -width 1
OkaySnap $who                           ;# See if it in correct position
    } else {                                    ;# Button move
set dx [expr {$x - $S(down,x)}]
set dy [expr {$y - $S(down,y)}]
.c move $who $dx $dy
    }
    set S(down,x) $x                            ;# Remember last position
    set S(down,y) $y
}
proc Mouse3 {who} {
    .c itemconfig $who -fill yellow
    set ::Y($who) 1                             ;# Mark as out of position
    RotateItem $who [Centroid $who] 45
    OkaySnap $who
}
proc Random {min max} {return [expr {$min + rand() * ($max - $min)}]}
proc Centroid {who} {
    foreach {x0 y0 x1 y1} [.c bbox $who] break
    return [list [expr {($x0 + $x1) / 2.0}] [expr {($y0 + $y1) / 2.0}]]
}
proc OkaySnap {who} {                           ;# See if close enough
    global C S Y

    foreach {p angles} $C(end,$who) break
    set n [lsearch $angles "a$C(rotate,$who)"]
    if {$n == -1} return
    set c [lindex $angles [expr {$n + 1}]]

    foreach {x1 y1} [lrange $C(board2)       [expr {2*$p}] [expr {2*$p+1}]] break
    foreach {x0 y0} [lrange [.c coords $who] [expr {2*$c}] [expr {2*$c+1}]] break
    set dx [expr {$x1 - $x0}]
    set dy [expr {$y1 - $y0}]
    set dist [expr {sqrt($dx*$dx + $dy*$dy)}]
    if {$dist > $S(snap)} return
    .c move $who $dx $dy
    .c itemconfig $who -fill green
    .c lower $who
    .c lower board
    snd_click play

    set Y($who) 0                               ;# Mark as in place
    foreach a [array names Y] {                 ;# Are we done?
if {$Y($a)} return
    }
    .c raise board
    .c itemconfig board -width 5 -dash {} -fill green
    set S(msg) "Done !"
}
proc DoSounds {} {
    proc snd_click {play} {}                    ;# Stub
    if {[catch {package require base64}]} return
    if {[catch {package require snack}]} return
    set sdata {UklGRkACAABXQVZFZm10IBAAAAABAAEAESsAABErAAABAAgAZGF0YRwCAACAgId0Z
HZbU5aMj7/MsIZ6UX6nWIiITWiIRUGUlZesvrGCiKiKTl96Fit3YF5emrGHqcqhlJuAdWxgW
01EbWSHubW1uJ2MkqGPYFVSamtvgHmEh5ybraWLkHp5Xm5oWGRvb3WSlYqMi4+JhY6Ac25xd
Xp5jYR/hoODdIN8e356goCHgoqGgIV/g35/d3N2eHZ6gIOIgouHioaNioGAfHpycHp2dH2Hi
ouNiYiKhIF9enZzd3l+dX2BgYKIjoaJhIJ/fX6AfHl8fICAgICEgISFhYF/gH+AfIJ/gH6Af
X6AfICAfYB+gn2DfoGAgIOAgYB8e3x9gIKChYCDgIN/g32Afn+BgIF+gH+BgIOAgX2CfYGAg
IB/gH9/fIB/gICBgH+Df4KAgIB9gHuBfYKAgoCAhICDgIN+gH+Af4CAgIGAg4CFgIOAgICAg
H9/f32AfoF/gn+BgICAf4B/gICAgICAgIKAgYCAgH+AfYB8f4CAgoGBgIKBgHt0cnqEi4yIh
oKHioOBeoF+gHRvbW10eYSHhoyMmI+PhIF5dm9tbW92fICJjpKRkY6JhHx5b2xlbnWAhYeOj
pSQkIiAe3R1cnNzdnx/gomLj4yJhICAfHp3d3d6fYKDhoKGgIeAhX1/eXt9foCAg4GCg4CDf
YF6gHmAfYCBgIR/h4CEf4B9fn98gHuEfYV/g4CAgn6Fe4R6gn1/gHuDe4V+g4CAgn8=}
    regsub -all {s} $sdata {} sdata            ;# Bug in base64 package
    sound snd_click
    snd_click data [::base64::decode $sdata]
}
proc Bird {} {
    global C

    set 2r2 [expr {2 * sqrt(2)}]
    catch {unset C}
    set C(board) [list 0 0 1 0 3 2 5 2 7 0 6 0 8 -2 5 -2 3 0 2 0 2 -1.5 1 -1.5]
    set C(scale) 60

    set C(coords,b1) {0 0 1 -1.5 2 -1.5 1 0}
    set C(end,b1) {0 {a0 0 a180 2}}
    set C(coords,b2) {0 0 1 -1.5 1 0}
    set C(end,b2) {1 {a0 0}}
    set C(coords,b3) {0 0 -2 -2 0 -2 2 0}
    set C(end,b3) {2 {a0 0 a180 2}}
    set C(coords,b4) [list $2r2 0 0 0 0 -$2r2]
    set C(end,b4) {3 {a45 0}}
    set C(coords,b5) {2 0 0 0 0 -2}
    set C(end,b5) {5 {a90 0}}
    set C(coords,b6) {2 0 0 0 0 -2}
    set C(end,b6) {3 {a90 0}}
    set C(coords,b7) {0 -1 1 -1 1 0 0 0}
    set C(end,b7) {7 {a0 0 a90 3 a180 2 a270 1}}
    set C(coords,b8) {1 0 0 0 0 -1 1 -1}
    set C(end,b8) {5 {a0 0 a90 3 a180 2 a270 1}}
}
proc Boat {} {
    global C

    set r2    [expr {sqrt(2)}]
    set r22   [expr {sqrt(2)/2}]
    set r22_1 [expr {sqrt(2)/2 + 1}]
    set r24   [expr {sqrt(2)/4}]
    catch {unset C}
    set C(scale) 110

    set C(board) {0 0}                          ;# P0
    AppendBoard -$r22 -$r22                     ;# P1
    AppendBoard [expr {$r22 + $r24/2}] 0        ;# P2
    AppendBoard 0 -$r24                         ;# P3
    AppendBoard 0 -$r22_1                       ;# P4
    AppendBoard $r22_1 $r22_1                   ;# P5
    AppendBoard [expr {-$r22_1 + $r24}] 0       ;# P6
    AppendBoard 0 $r24                          ;# P7
    AppendBoard [expr {$r24/2 + $r2 + $r22}] 0  ;# P8
    AppendBoard -$r22 $r22                      ;# P9
    AppendBoard -$r22 0                         ;# P10

    set C(coords,b1) [list 0 0 0 -$r22 $r22 0]
    set C(end,b1) {3 {a0 0}}
    set C(coords,b2) [list 0 0 0 -$r22 $r22 0]
    set C(end,b2) {0 {a180 1}}
    set C(coords,b3) [list 0 0 0 -$r22 $r22 0]
    set C(end,b3) {10 {a0 2}}
    set C(coords,b4) [list 0 0 0 -1 1 0]
    set C(end,b4) {10 {a315 0}}
    set C(coords,b5) [list 0 0 0 -1 1 0]
    set C(end,b5) {5 {a0 2}}
    set C(coords,b6) [list 0 0 $r22 $r22 $r22 $r22_1 0 1]
    set C(end,b6) {4 {a0 0 a180 2}}
    set C(coords,b7) [list 0 0 $r22 -$r22 [expr {$r22+$r22}] -$r22 $r22 0]
    set C(end,b7) {10 {a0 0 a180 2}}
    set C(coords,b8) [list 0 0 0 -$r22 $r22 -$r22 $r22 0]
    set C(end,b8) {0 {a0 0 a90 3 a180 2 a270 1}}
    set C(coords,b9) [list 0 0 0 -$r24 $r24 -$r24 $r24 0]
    set C(end,b9) {3 {a0 1 a90 0 a180 3 a270 2}}
}
proc AppendBoard {dx dy} {
    foreach {x y} [lrange $::C(board) end-1 end] break ;# Last point in list
    set x [expr {round(10000 * ($x + $dx)) / 10000.0}]
    set y [expr {round(10000 * ($y + $dy)) / 10000.0}]
    lappend ::C(board) $x $y
}
DoDisplay
DoSounds
if {[expr {rand()}] > .5} { set what Bird } else { set what Boat }
GoPuzzle $what