Labyrinth

Keith Vetter 2005-11-18 : Somehow my family acquired the board game Junior Labyrinth. We have a lot of fun playing it despite not having any instructions and lacking a few pieces.

I thought I'd try writing a tcl version of the game. This was one of those fun projects that started off small and incrementally grew bigger as I added just one more feature. Initially it was just the sliding tiles (see also Shifting Maze), then stippling for the brick look, then moving players, then.... The next thing I knew I had a complete game.

Except that I still don't know how the game is officially played, so I used the rules that we use in our house. The objective is to be the first player to collect 15 gems. The players rotate taking turns. A players turn consists of two parts, first sliding a tile to change the maze and then moving the piece to capture the gem. A player's turn is over when he either captures the gem or he presses the DONE button.

AK: I described the rules I know on the Shifting Maze page.


uniquename 2013aug01

This game deserves an image to show what everyone is talking about here.

vetter_LabyrinthGame_screenshot_661x601.jpg

This static image does not show that the arrows are blinking on and off.


 ##+##########################################################################
 #
 # Labyrinth.tcl -- Plays Junior Labyrinth
 # by Keith Vetter, Nov 2005
 #

 package require Tk

 set S(title) "Junior Labyrinth"
 set S(version) "1.02"
 set S(sz)  65                                   ;# Tile size: 100, For screen resolution 1024x768: 65
 set S(wall) [expr {$S(sz) / 4.0}]               ;# Wall thickness
 set S(pad) 2                                    ;# Space between tiles
 set S(m) $S(sz)                                 ;# Margin
 set S(n) 5                                      ;# How many rows and columns
 set S(nn) [expr {$S(n)-1}]
 set S(bsize) [expr {$S(n)*$S(sz) + $S(nn)*$S(pad)}]
 set S(csize) [expr {2*$S(m)      + $S(bsize)}]
 set S(blink,on) 2000
 set S(blink,off) 500
 set S(delay)  10                                ;# Time between animation steps
 set S(step)    2                                ;# Animation step size
 set S(goal)   15                                ;# Winning total

 set S(players) 2                                ;# How many players
 set S(state) pick
 set S(key) ""
 set S(turn) [expr {$S(players)-1}]
 set S(lastShift) {0 0}

 array set COLORS {
    board yellow . saddlebrown bg green4 arrow yellow txt deepskyblue gem skyblue
    brick red mortar black score,bg black score,fg white
    player,0 magenta  player,1 green3  player,2 cyan  player,3 red
 }

 array set TILES {corner 8 tee 7 line 2}
 set FIXED {0 0 rb 0 2 lbr 0 4 lb 2 0 trb 2 2 lr 2 4 tlb 4 0 tr 4 2 trl 4 4 tl}
 array set RAND {c {tr tl rb lb} t {trl trb rlb tlb} l {lr tb}}
 array set DIR  {Up {-1 0} Down {1 0} Left {0 -1} Right {0 1}}
 array set DIR2 {1,0 Down -1,0 Up 0,1 Right 0,-1 Left}
 array set SCORE {0 0 1 0 2 0 3 0}
 set PI [expr {acos(-1)}]

 proc DoDisplay {} {
    global S COLORS
    option add *Canvas.highlightThickness 0

    wm title . $S(title)
    . config -bg $COLORS(.)
    DoMenus

    GetBoxesBMP
    frame .s -bg $COLORS(score,bg) -bd 2 -relief ridge -padx 5

    set w [expr {$S(m) + $S(bsize) + $S(m)}]
    set h [expr {$S(m) + $S(bsize) + $S(m)}]

    canvas     .title -width $S(csize) -bd 0 -bg $COLORS(bg)
    ShadedText .title [expr {$S(csize)/2}] 10 $COLORS(txt) black \
        -font {Times 42 bold} -anchor n -tag title -text $S(title)
    .title config -height [lindex [.title bbox title] 3]

    canvas .c -width $S(csize) -height $S(csize) -bd 0 -bg $COLORS(bg)
    .c create rect -10 -10 10000 10000 -fill $COLORS(bg) -tag bg
    image create photo ::img::rot -data $::rotImage
    button .rot -image ::img::rot -command RotateTile
    .c create window [LocateTile rotate rotate 1] -tag rotate -window .rot
    button .done -text "Done" -font {Helvetica 12 bold} \
        -command {NewState done} -height 2
    .c create window [LocateTile extra extra 1] -tag done -window .done

    MakeScoreArea
    label .msg -textvariable S(msg) -font {Times 32 bold} -bg $COLORS(bg)

    foreach {x0 y0} [LocateTile 0 0] break
    foreach {. . x1 y1} [LocateTile $S(nn) $S(nn)] break
    .c create rect $x0 $y0 $x1 $y1 -tag board -fill $COLORS(board) \
        -outline $COLORS(board)

    foreach {r c d} {-1 1 s -1 3 s 5 1 n 5 3 n 1 -1 e 3 -1 e 1 5 w 3 5 w} {
        MakeArrow $r $c $d
    }
    NewBoard

    pack .s     -side right -fill y
    pack .title -side top -fill x
    pack .c     -side top -fill both -expand 1 -pady 24 -padx 24 \
        -ipadx 5 -ipady 5
    pack .msg   -side bottom -fill x
    foreach key {Up Down Left Right} {
        bind .c <KeyPress-$key>   [list KeyPress %K press]
        bind .c <KeyRelease-$key> [list KeyPress %K release]
    }
    bind all <Key-F2> {console show}
    focus .c
    wm geom . +5+5
 }
 proc DoMenus {} {
    option add *Menu.tearOff 0
    menu .menu
    . config -menu .menu

    menu .menu.game
    .menu add cascade -label "Game" -menu .menu.game -underline 0
    .menu.game add command -label "New Game" -command NewGame

    set m .menu.game.players
    menu $m
    .menu.game add cascade -label "Players" -menu $m -underline 0
    foreach n {2 3 4} {
        $m add radio -label "$n Players" \
            -variable S(players) \
            -value $n \
            -underline 0 \
            -command NewGame
    }
    .menu.game add separator
    .menu.game add command -label "Exit" -command exit

    menu .menu.help
    .menu add cascade -label "Help" -menu .menu.help -underline 0
    .menu.help add command -label "Help"  -command Help
    .menu.help add command -label "About" -command About
 }
 proc MakePlayers {} {
    foreach {who row col} {0 0 0 1 0 4 2 4 0 3 4 4} {
        .c delete player,$who
        if {$who >= $::S(players)} continue
        DrawPlayer $who $row $col
        set ::PLAYERS($who) [list $row $col]

        .c bind player,$who <ButtonPress-1>   [list BDown $who]
        .c bind player,$who <B1-Motion>       [list BMotion $who %x %y]
        .c bind player,$who <ButtonRelease-1> [list BUp $who]
    }
 }
 proc MakeScoreArea {} {
    global S COLORS SCORE

    eval destroy [winfo child .s]
    set csize 75

    label .s.title -text Score -font {Times 42 bold underline} \
        -bg $COLORS(score,bg) -fg $COLORS(score,fg)
    grid .s.title - -sticky ew -row 1
    for {set who 0} {$who < $S(players)} {incr who} {
        canvas .s.$who -width $csize -height $csize \
            -bg $COLORS(score,bg) -bd 5 -relief flat
        DrawPlayerAt 10 10 $csize $csize $COLORS(player,$who) tag .s.$who
        label .s.l$who -textvariable SCORE($who) -font {Times 36 bold} \
            -bg $COLORS(score,bg) -fg $COLORS(score,fg) -width 3
        grid .s.$who .s.l$who -sticky news -pady 20
    }
    grid rowconfigure .s 60 -weight 1
 }
 proc ShadedText {w x y fg bg args} {
    set cbg [ $w cget -bg ]
    eval [list $w create text $x $y -fill $bg] $args
    eval [list $w create text [incr x -2] [incr y -2] -fill $cbg] $args
    eval [list $w create text [incr x -1] [incr y -1] -fill  $fg] $args
 }
 proc FillBoard {} {
    global S FIXED BOARD TILES RAND

    .c delete win
    unset -nocomplain BOARD
    set id -1
    foreach {row col doors} $FIXED {
        MakeTile "fixed,[incr id]" [LocateTile $row $col] $doors
        set BOARD(doors,$row,$col) $doors
    }
    set S(deck) [Shuffle [concat [string repeat "c " $TILES(corner)] \
                              [string repeat "t " $TILES(tee)] \
                              [string repeat "l " $TILES(line)]]]
    set idx -1
    for {set row 0} {$row < $S(n)} {incr row} {
        for {set col 0} {$col < $S(n)} {incr col} {
            if {[info exists BOARD(doors,$row,$col)]} continue

            set type [lindex $S(deck) [incr idx]]
            set doors [lindex $RAND($type) \
                           [expr {int(rand() * [llength $RAND($type)])}]]
            MakeTile "tile,$idx" [LocateTile $row $col] $doors
            set BOARD(doors,$row,$col) $doors
            set BOARD(tag,$row,$col) "tile,$idx"
        }
    }

    set type [lindex $S(deck) [incr idx]]
    set doors [lindex $RAND($type) \
                   [expr {int(rand() * [llength $RAND($type)])}]]
    MakeTile "tile,$idx" [LocateTile extra extra] $doors
    set BOARD(doors,extra) $doors
    set BOARD(tag,extra) "tile,$idx"
 }
 proc LocateTile {row col {mid 0}} {
    global S

    if {$row eq "extra"} {
        return [LocateTile $S(n) $S(n) $mid]
    }
    if {$row eq "rotate"} {
        return [LocateTile $S(n) $S(nn) $mid]
    }

    set x0 [expr {$S(m) + $col*($S(sz)+$S(pad))}]
    set y0 [expr {$S(m) + $row*($S(sz)+$S(pad))}]
    if {$mid} {
        return [list [expr {$x0 + $S(sz)/2}] [expr {$y0 + $S(sz)/2}]]
    }
    set x1 [expr {$x0 + $S(sz)}]
    set y1 [expr {$y0 + $S(sz)}]
    return [list $x0 $y0 $x1 $y1]
 }
 proc Canvas2Tile {x y} {
    global S

    set sz [expr {$S(sz) + $S(pad)}]
    set row [expr {int(($y - $S(m) + $S(pad)/2 - 1) / $sz)}]
    set col [expr {int(($x - $S(m) + $S(pad)/2 - 1) / $sz)}]
    return [list $row $col]
 }
 proc MakeArrow {row col dir} {
    array set D {
        s {2 1 2 4}
        n {2 3 2 0}
        e {1 2 4 2}
        w {3 2 0 2}
    }

    foreach {x(0) y(0) x(4) y(4)} [LocateTile $row $col] break

    set x(1) [expr { $x(0) + ($x(4)-$x(0))/4}]
    set x(2) [expr {($x(0) + $x(4))/2}]
    set x(3) [expr { $x(4) - ($x(4)-$x(0))/4}]
    set y(1) [expr { $y(0) + ($y(4)-$y(0))/4}]
    set y(2) [expr {($y(0) + $y(4))/2}]
    set y(3) [expr { $y(4) - ($y(4)-$y(0))/4}]

    set xy {}
    foreach {dx dy} $D($dir) {
        lappend xy $x($dx) $y($dy)
    }
    set id [.c create line $xy -tag [list arrow a$row,$col] \
                -width 10 -capstyle round \
                -fill $::COLORS(arrow) -arrow last -arrowshape {16 24 11}]
    .c bind $id <1> [list Shift $row $col]
 }
 proc MakeTile {tag rect doors} {
    global S COLORS
    array set PARTS {
        lr  {n s}
        bt  {e w}
        br  {Lnw se}
        bl  {Lne sw}
        rt  {Lsw ne}
        lt  {Lse nw}
        lrt {s nw ne}
        brt {w ne se}
        blr {n se sw}
        blt {e nw sw}
    }

    .c delete $tag
    .c create rect $rect -width 0 -fill $COLORS(board) -tag $tag
    set doors [join [lsort [split $doors ""]] ""]
    foreach part $PARTS($doors) {
        set xy [GetSubCoords $rect $part]
        .c create poly $xy -tag $tag -fill $COLORS(brick)  -outline $COLORS(mortar)
        .c create poly $xy -tag $tag -fill $COLORS(mortar) -stipple @$S(bmp) -offset n
    }
 }
 proc GetSubCoords {rect what} {
    array set XY {
        n   {$x0 $y0 $x1 $y0 $x1 $yq1 $x0 $yq1}
        s   {$x0 $yq2 $x1 $yq2 $x1 $y1 $x0 $y1}
        w   {$x0 $y0 $xq1 $y0 $xq1 $y1 $x0 $y1}
        e   {$xq2 $y0 $x1 $y0 $x1 $y1 $xq2 $y1}
        ne  {$xq2 $y0 $x1 $y0 $x1 $yq1 $xq2 $yq1}
        nw  {$x0 $y0 $xq1 $y0 $xq1 $yq1 $x0 $yq1}
        se  {$xq2 $yq2 $x1 $yq2 $x1 $y1 $xq2 $y1}
        sw  {$x0 $yq2 $xq1 $yq2 $xq1 $y1 $x0 $y1}
        Lsw {$x0 $y0 $xq1 $y0 $xq1 $yq2 $x1 $yq2 $x1 $y1 $x0 $y1}
        Lnw {$x0 $y0 $x1 $y0 $x1 $yq1 $xq1 $yq1 $xq1 $y1 $x0 $y1}
        Lne {$x0 $y0 $x1 $y0 $x1 $y1 $xq2 $y1 $xq2 $yq1 $x0 $yq1}
        Lse {$xq2 $y0 $x1 $y0 $x1 $y1 $x0 $y1 $x0 $yq2 $xq2 $yq2}
    }

    foreach {x0 y0 x1 y1} $rect break
    set xq1 [expr {$x0+$::S(wall)}]
    set xq2 [expr {$x1-$::S(wall)}]
    set yq1 [expr {$y0+$::S(wall)}]
    set yq2 [expr {$y1-$::S(wall)}]

    set xy [subst -nocommands -nobackslashes $XY($what)]
    return $xy
 }
 proc Shuffle { l } {
    set len [llength $l]
    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 $l $i]
        lset l $i [lindex $l $n]
        lset l $n $temp
    }
    return $l
 }
 proc HideLastArrow {} {
    foreach {row col} $::S(lastShift) break
    if {$row == -1 || $row eq $::S(n)} {
        set row [expr {$row == -1 ? $::S(n) : -1}]
    } else {
        set col [expr {$col == -1 ? $::S(n) : -1}]
    }
    .c lower a$row,$col
 }
 proc NewBoard {} {
    FillBoard
    MakePlayers
    RandomGem
 }
 proc NewState {new} {
    global S COLORS BOARD SCORE

    if {$new eq "gem"} {
        BUp $S(turn)
        KillGem
        incr SCORE($S(turn))
        if {$SCORE($S(turn)) >= $S(goal)} {
            Winner $S(turn)
            set S(state) win
            .c itemconfig done -window {}
            return
        }
        RandomGem
        set new done
    }

    if {$new eq "done"} {
        .s.$S(turn) config -relief flat
        set S(turn) [expr {($S(turn)+1) % $S(players)}]
        .s.$S(turn) config -relief ridge
        #.s.cturn itemconfig player -fill $COLORS(player,$S(turn)) \
            -outline $COLORS(player,$S(turn))
        set S(msg) "Click Arrow to Slide Tiles"
        .c raise arrow bg
        .c raise $BOARD(tag,extra) bg
        .c raise player,$S(turn)
        .c raise gem
        HideLastArrow
        .c itemconfig rotate -window .rot
        .c itemconfig done -window {}
        set S(state) pick
        BlinkArrows 0
    } else {
        set S(state) $new
        .c lower arrow bg
        .c lower $BOARD(tag,extra) bg
        .c itemconfig rotate -window {}
        if {$S(state) eq "move"} {
            set S(msg) "Move Player to Capture Gem"
            .c itemconfig done -window .done
        }
    }
 }
 proc Shift {row col} {
    if {$::S(state) ne "pick"} return
    NewState shift
    set ::S(lastShift) [list $row $col]
    if {$row == -1} { ShiftCol $col 1 }
    if {$row == $::S(n)} { ShiftCol $col -1 }

    if {$col == -1} { ShiftRow $row 1 }
    if {$col == $::S(n)} { ShiftRow $row -1 }
    NewState move
 }
 proc ShiftRow {row dir} {
    if {$dir == 1} {
        MoveTileTo $::BOARD(tag,extra) $row -1
        set u {extra save 4 extra 3 4 2 3 1 2 0 1 save 0}
    } else {
        MoveTileTo $::BOARD(tag,extra) $row 5
        set u {0 save 1 0 2 1 3 2 4 3 extra 4 save extra}
    }
    set tags    [GetRowColTags   row $row]
    set players [PlayersOnRowCol row $row]
    foreach player $players { lappend tags "player,$player" }
    set gems [GemsOnRowCol row $row]
    foreach tag $gems { lappend tags $tag }
    update; after 500
    DoShift $tags $dir 0
    vwait ::S(vwait)

    foreach {from to} $u {
        set from [Index $row $from]
        set to [Index $row $to]
        UpdateBoard $from $to
    }
    MoveTileTo $::BOARD(tag,extra) extra extra
    UpdatePlayers $players $dir 0
    UpdateGem $gems $dir 0
 }
 proc ShiftCol {col dir} {
    if {$dir == 1} {
        MoveTileTo $::BOARD(tag,extra) -1 $col
        set u {extra save  4 extra 3 4 2 3 1 2 0 1 save 0}
    } else {
        MoveTileTo $::BOARD(tag,extra) $::S(n) $col
        set u {0 save 1 0 2 1 3 2 4 3 extra 4 save extra}
    }
    set tags [GetRowColTags col $col]
    set players [PlayersOnRowCol col $col]
    foreach player $players { lappend tags "player,$player" }
    set gems [GemsOnRowCol col $col]
    foreach tag $gems { lappend tags $tag }
    update ; after 500
    DoShift $tags 0 $dir
    vwait ::S(vwait)

    foreach {from to} $u {
        set from [Index $from $col]
        set to [Index $to $col]
        UpdateBoard $from $to
    }
    MoveTileTo $::BOARD(tag,extra) extra extra
    UpdatePlayers $players 0 $dir
    UpdateGem $gems 0 $dir
 }
 proc UpdateGem {who dx dy} {
    if {$who eq {}} return
    foreach {r c} [split $::GEM ","] break
    incr r $dy
    incr c $dx
    set off 0
    if {$r < 0} { set off 1 ; set r $::S(nn)}
    if {$r > $::S(nn)} { set off 1 ; set r 0}
    if {$c < 0} { set off 1 ; set c $::S(nn)}
    if {$c > $::S(nn)} { set off 1 ; set c 0}
    set ::GEM "$r,$c"
    if {$off} { DrawGem $r $c }
 }
 proc UpdatePlayers {who dx dy} {
    foreach player $who {
        foreach {r c} $::PLAYERS($player) break
        incr r $dy
        incr c $dx
        set off 0
        if {$r < 0} { set off 1 ; set r $::S(nn)}
        if {$r > $::S(nn)} { set off 1 ; set r 0}
        if {$c < 0} { set off 1 ; set c $::S(nn)}
        if {$c > $::S(nn)} { set off 1 ; set c 0}
        set ::PLAYERS($player) [list $r $c]
        if {$off} { DrawPlayer $player $r $c }
    }
 }
 proc PlayersOnRowCol {what which} {
    set cells [CellsOnRowCol $what $which]
    set result {}
    for {set player 0} {$player < $::S(players)} {incr player} {
        foreach {r c} $::PLAYERS($player) break
        set n [lsearch $cells "$r,$c"]
        if {$n != -1} { lappend result $player }
    }
    return $result
 }
 proc GemsOnRowCol {what which} {
    set cells [CellsOnRowCol $what $which]
    if {[lsearch $cells $::GEM] != -1} { return gem}
    return {}
 }
 proc CellsOnRowCol {what which} {
    set cells {}
    for {set idx 0} {$idx < $::S(n)} {incr idx} {
        if {$what eq "row"} {
            lappend cells $which,$idx
        } else {
            lappend cells $idx,$which
        }
    }
    return $cells
 }
 proc GetRowColTags {what who} {
    set tags $::BOARD(tag,extra)
    for {set idx 0} {$idx < $::S(n)} {incr idx} {
        if {$what eq "row"} {
            lappend tags $::BOARD(tag,$who,$idx)
        } else {
            lappend tags $::BOARD(tag,$idx,$who)
        }
    }
    return $tags
 }
 proc UpdateBoard {from to} {
    global BOARD

    set BOARD(doors,$to) $BOARD(doors,$from)
    set BOARD(tag,$to)   $BOARD(tag,$from)
 }
 proc Index {row col} {
    if {$row eq "extra" || $col eq "extra"} { return "extra"}
    if {$row eq "save"  || $col eq "save"}  { return "save"}
    return "$row,$col"
 }
 proc DoShift {tags dx dy {fast 0} {soFar 0}} {
    set dd [expr {$fast ? 3*$::S(step) : $::S(step)}]
    set max [expr {$::S(sz) + $::S(pad)}]
    if {$soFar >= $max} { set ::S(vwait) 1 ; return}
    incr soFar $dd
    if {$soFar > $max} { set dd [expr {$dd + $max - $soFar}]}

    set dxx [expr {$dd*$dx}]
    set dyy [expr {$dd*$dy}]
    foreach tag $tags {
        .c move $tag $dxx $dyy
    }
    after $::S(delay) [list DoShift $tags $dx $dy $fast $soFar]
 }
 proc MoveTileTo {id row col} {
    foreach {x1 y1} [.c coords $id] break
    foreach {x2 y2} [LocateTile $row $col] break

    set dx [expr {$x2 - $x1}]
    set dy [expr {$y2 - $y1}]
    .c move $id $dx $dy
    .c raise $id board
 }
 proc DrawPlayer {who row col} {
    global S COLORS

    .c delete player,$who
    set pad [expr {-$S(wall)-2}]
    foreach {x0 y0 x1 y1} [Expand [LocateTile $row $col] $pad] break
    DrawPlayerAt $x0 $y0 $x1 $y1 $COLORS(player,$who) player,$who
    .c move player,$who [expr {2*($who-1)}] 0
 }
 proc DrawPlayerAt {x0 y0 x1 y1 color tag {W .c}} {
    set w [expr {$x1 - $x0}]
    set h [expr {$y1 - $y0}]

    set xm [expr {($x1 + $x0)/2}]
    set ym [expr {($y1 + $y0)/2}]

    set w8 [expr {$h/8}]
    set cy [expr {$y0 + $w8}]
    set cxy [Expand [list $xm $cy $xm $cy] $w8]

    set mxy [list $xm $cy \
                 [expr {$xm-1*$w/4}] $ym \
                 [expr {$xm-1*$w/8}] $ym \
                 [expr {$xm-3*$w/8}] $y1 \
                 [expr {$xm+3*$w/8}] $y1 \
                 [expr {$xm+1*$w/8}] $ym \
                 [expr {$xm+1*$w/4}] $ym \
                 $xm $cy]
    $W create poly $mxy -tag $tag -fill $color -outline $color
    $W create oval $cxy -tag $tag -fill $color -outline $color
 }
 proc DrawGem {row col} {
    global S COLORS

    .c delete gem
    set pad [expr {-$S(wall)-2}]
    foreach {x0 y0 x1 y1} [Expand [LocateTile $row $col] $pad] break
    DrawGemAt ? $x0 $y0 $x1 $y1 $COLORS(gem) gem
 }
 proc DrawGemAt {which x0 y0 x1 y1 color tag {W .c}} {
    set D(0) {
        {3 0 3 3 0 3}
        {3 0 3 3 6 3}
        {3 6 3 3 0 3}
        {3 6 3 3 6 3}
    }
    set D(1) {
        {2 1 4 1 5 2 5 4 4 5 2 5 1 4 1 2}
        {0 1 1 0 2 1 1 2}
        {1 0 5 0 4 1 2 1}
        {0 1 1 2 1 4 0 5}
        {5 0 6 1 5 2 4 1}
        {1 4 2 5 1 6 0 5}
        {2 5 4 5 5 6 1 6}
        {6 1 6 5 5 4 5 2}
        {5 4 6 5 5 6 4 5}
    }
    set D(2) {
        {1 0 2 0 2 1 0 1}
        {3 6 0 1 2 1}
        {2 0 4 0 4 1 2 1}
        {3 6 2 1 4 1}
        {4 0 5 0 6 1 4 1}
        {3 6 4 1 6 1}
    }
    set D(3) {
        {1 0 2 2 0 1}
        {1 0 5 0 4 2 2 2}
        {0 1 2 2 2 4 0 5}
        {5 0 6 1 4 2}
        {2 2 4 2 4 4 2 4}
        {2 4 1 6 0 5}
        {6 1 6 5 4 4 4 2}
        {2 4 4 4 5 6 1 6}
        {4 4 6 5 5 6}
    }
    if {$which eq "?"} {
        set which [expr {int(rand() * [llength [array names D]])}]
    }
    if {$which != 0} {
        foreach {x0 y0 x1 y1} [Expand [list $x0 $y0 $x1 $y1] -2] break
    }
    for {set i 0} {$i < 7} {incr i} {           ;# Get every 1/6 interval
        set x($i) [expr {$x0 + $i * ($x1-$x0)/6}]
        set y($i) [expr {$y0 + $i * ($y1-$y0)/6}]
    }

    set idx -1
    set darken [expr {70 / [llength $D($which)]}]
    foreach coords $D($which) {
        incr idx
        set xy(x,$idx) {}
        foreach {a b} $coords {
            lappend xy(x,$idx) $x($a) $y($b)
        }
        set c [::tk::Darken $color [expr {110-$darken*$idx}]]
        $W create poly $xy(x,$idx) -fill $c -tag [list $tag gem$idx] \
            -outline black
    }
 }
 proc KillGem {} {
    foreach {x0 y0 x1 y1} [.c bbox gem] break
    set xrad [expr {($x1 - $x0)/2}]
    set yrad [expr {($y1 - $y0)/2}]
    set xm   [expr {($x1 + $x0)/2}]
    set ym   [expr {($y1 + $y0)/2}]

    while {1} {
        .c scale gem $xm $ym .95 .95
        update
        foreach {l . r} [.c bbox gem] break
        if {$r - $l < 15} break
        after 30
    }
    .c delete gem
    foreach step {.25 .5 .75} rad {1 2 3} {
        for {set theta 0} {$theta < 360} {incr theta 60} {
            set x [expr {$xm + $step*$xrad*cos($theta * $::PI/180)}]
            set y [expr {$ym + $step*$yrad*sin($theta * $::PI/180)}]
            set xy [Expand [list $x $y] $rad]
            .c create oval $xy -tag gem -fill $::COLORS(gem)
        }
        update
        after 30
        .c delete gem
    }
 }
 proc RandomGem {} {
    global S GEM PLAYERS COLORS
    set bad {}
    for {set who 0} {$who < $S(players)} {incr who} {
        lappend bad [join $PLAYERS($who) ","]
    }

    while {1} {
        set row [expr {int(rand() * $S(n))}]
        set col [expr {int(rand() * $S(n))}]
        set n [lsearch $bad "$row,$col"]
        if {$n == -1} break
    }
    set COLORS(gem) [LightColor]
    DrawGem $row $col
    set GEM "$row,$col"
 }
 proc Expand {xy d} {
    foreach {x0 y0 x1 y1} [concat $xy $xy] break
    return [list [expr {$x0-$d}] [expr {$y0-$d}] \
                 [expr {$x1+$d}] [expr {$y1+$d}]]
 }
 proc MovePlayer {who dir {fast 0}} {
    global S PLAYERS BOARD DIR GEM

    if {$S(state) ne "move"} return
    NewState "moving"

    while {1} {
        foreach {row col} $PLAYERS($who) break
        foreach {drow dcol} $DIR($dir) break

        set row2 [expr {$row + $drow}]
        set col2 [expr {$col + $dcol}]

        # Check legal move: on board w/o a wall
        if {$row2 < 0 || $row2 >= $S(n)|| $col2 < 0 || $col2 >= $S(n)} break
        set door [string map {U t D b R r L l} [string range $dir 0 0]]
        if {[string first $door $BOARD(doors,$row,$col)] == -1} break
        set door [string map {t b b t r l l r} $door]
        if {[string first $door $BOARD(doors,$row2,$col2)] == -1} break

        DoShift player,$who $dcol $drow $fast
        vwait ::S(vwait)

        set PLAYERS($who) [list $row2 $col2]

        if {$GEM eq "$row2,$col2"} {
            NewState gem
            return
        }
        if {$S(key) eq "" || $S(key) eq "mouse"} break
        set dir $S(key)
    }
    NewState "move"
 }
 proc KeyPress {who how} {
    global S

    if {$how eq "release" && $S(key) eq $who} {
        set S(key) ""
    } elseif {$how eq "press" && $S(key) ne $who && $S(key) ne "mouse"} {
        set S(key) $who
        if {$S(state) eq "move"} {
            after 1 MovePlayer $S(turn) $who
        }
    }
 }
 #
 # Stippling w/ custom bitmaps seems to require the bmp to be saved in
 # the file system. Here we write the bmp file to the tmp directory.
 #
 proc GetBoxesBMP {} {
    global S

    set boxesBMP {
        #define boxes_width 11
        #define boxes_height 9
        static char boxes_bits = {
            0xff, 0x07, 0xff, 0x07, 0x60, 0x00, 0x60, 0x00, 0xff,
            0x07, 0xff, 0x07, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00
        }
    }
    set bmpName "JLBoxes.bmp"

    if {[file exists $bmpName]} {
        set S(bmp) $bmpName
        return
    }
    switch $::tcl_platform(platform) {
        unix {
            set tmpdir /tmp   ;# or even $::env(TMPDIR), at times.
        } macintosh {
            set tmpdir $::env(TRASH_FOLDER)  ;# a better place?
        } default {
            set tmpdir [pwd]
            catch {set tmpdir $::env(TMP)}
            catch {set tmpdir $::env(TEMP)}
        }
    }
    set fname [file join $tmpdir $bmpName]
    if {[file exists $fname]} {
        set S(bmp) $fname
        return
    }
    catch {
        set fout [open $fname w]
        puts $fout $boxesBMP
        close $fout
    }
    if {[file exists $fname]} {
        set S(bmp) $fname
        return
    }

    set emsg "ERROR: cannot create brick bitmap"
    tk_messageBox -title $S(title) -icon error -message $emgs
    exit
 }
 proc BlinkArrows {cnt} {
    global S COLORS

    if {$S(state) ne "pick"} return
    if {[incr cnt] > 31} return
    set col [expr {$cnt & 1 ? $COLORS(arrow) : $COLORS(bg)}]
    .c itemconfig arrow -fill $col
    after $S(blink,[expr {$cnt & 1 ? "on" : "off"}]) [list BlinkArrows $cnt]
 }
 ##+##########################################################################
 #
 # LightColor -- returns a "light" color. A light color is one in which
 # the V value in the HSV color model is greater than .7. Since the V
 # value is the maximum of R,G,B we simply need at least one of R,G,B
 # must be greater than .7.
 #
 proc LightColor {} {
    set light [expr {255 * .7}]                 ;# Value threshold
    while {1} {
        set r [expr {int (255 * rand())}]
        set g [expr {int (255 * rand())}]
        set b [expr {int (255 * rand())}]
        if {$r > $light || $g > $light || $b > $light} break
    }
    return [format "\#%02x%02x%02x" $r $g $b]
 }
 proc RotateTile {} {
    global BOARD

    set BOARD(doors,extra) [string map {r b b l l t t r} $BOARD(doors,extra)]
    MakeTile $BOARD(tag,extra) [LocateTile extra extra] $BOARD(doors,extra)
 }

 set rotImage {
    R0lGODlhLgAqALMAABQWjJCQmMvMy0tKfwQDyayprO7t7nRzdCQmdHR2jwQC+wkHqi0rmdnb2ba4
    tvz+/CH5BAAAAAAALAAAAAAuACoAAwT/8MlJq7046827/2CYIchwBIL4OGzrvmwjGYtiKwsTyN+x
    EMCgcJiY1W43QMLwCRCQUGjx0ThGFQjHp2C93qaNpxcX+DgA469EIB4Tph3BIK0AI35pwqEJaEfh
    DwYCCQNdSARlHAZ9eFeAFA5zVwspG5I4hjaPFQd+NwMbAl0LmZsVAZkEWhmXN6RSHE5RoBiLUQgF
    aGocrTYLPBYOnqoPAgi7G8JRiRYJUQATDZKmF8ewF70KexMGA28ezlC0FtnMEwfUFmxQDBgMUQUX
    THGeC+5QBPEqElVQ9tjL9q3x1A7gtX3hkBRs9myeimzjKhQYpk+EqIDydCFBsC/brwwHeKJ8CzFx
    loZ1/ip2cJBKpcGUHs5ciYihgcZDB4BdaJAgE45KG0pOGlCgwTwDBgocuHnI3IYEnvwxYDCAQSMv
    NHnR2frJoQcDULm6GeAVRACmYnGk8yDN51igAtck6EMHwAG4cSkYcBCgqtUcVBMUKJu3sOHDiPNG
    AAA7
 }
 proc NewGame {} {
    foreach aid [after info] { after cancel $aid}
    MakeScoreArea
    NewBoard
    array set ::SCORE {0 0 1 0 2 0 3 0}
    set ::S(turn) [expr {$::S(players)-1}]
    NewState done
 }
 proc About {} {
    set msg "$::S(title) v$::S(version)\n\nby Keith Vetter\nNovember 2005\n"
    tk_messageBox -title "About $::S(title)" -message $msg
 }
 proc Help {} {
    global S

    catch {destroy .help}
    toplevel .help
    wm title .help "$S(title) Help"

    set t .help.t
    text $t -relief raised -wrap word -width 60 -height 23 \
        -padx 10 -pady 10 -cursor {}
    button .help.ok -text OK -width 8 -command {destroy .help}
    pack .help.ok -side bottom -pady 10
    pack $t -side top -expand 1 -fill both

    set bold   "[font actual [$t cget -font]] -weight bold"
    set italic "[font actual [$t cget -font]] -slant italic"
    $t tag config title -justify center -foregr red -font "Arial 20 bold"
    $t tag configure title2 -justify center -font "Arial 12 bold"
    $t tag configure heading -font $bold
    $t tag configure n -lmargin1 10 -lmargin2 10
    $t tag configure bullet -lmargin1 20 -lmargin2 30

    $t insert end "$S(title)\n" title
    $t insert end "by Keith Vetter\n\n" title2

    $t insert end "Based on a children's game by Ravensburger.\n\n"

    set h "Objective\n"
    set m "To be the first player to collect $S(goal) gems.\n\n"
    $t insert end $h heading $m n

    set h "Starting a New Game\n"
    set b "o Select Game->New Game\n"
    append b "o Select Game->Players to change the number of players\n\n"
    $t insert end $h heading $b bullet

    #Playing
    set h "Playing the Game\n"
    set m "The players rotate taking turns. A player's turn consists "
    append m "of two parts:\n"
    set b "1. Sliding a tile to change the maze.\n"
    append b "2. Moving the player to try to capture the gem.\n\n"
    set m2 "A players turn ends when:\n"
    set b2 "o The gem is captured.\n"
    append b2 "o The player presses the DONE button.\n\n"
    $t insert end $h heading $m n $b bullet $m2 n $b2 bullet

    $t config -state disabled
    focus $t
 }
 proc Winner {who} {
    global S COLORS

    foreach {x0 y0 x1 y1} [LocateTile [expr {$S(n)/2}] [expr {$S(n)/2-1}]] break
    DrawPlayerAt $x0 $y0 $x1 $y1 $COLORS(player,$who) win .c
    set ym [expr {($y1 + $y0)/2}]
    .c create text $x1 $ym -tag win -text "Wins!" -font {Times 42 bold} \
        -fill white -anchor w
    set xy [Expand [.c bbox win] 30]
    .c create rect $xy -fill black -outline white -width 10 -tag {win x}
    .c lower x win
    set S(msg) ""
 }
 proc BDown {who} {
    if {$::S(turn) != $who} return
    if {$::S(state) ne "move"} return

    set color [::tk::Darken $::COLORS(player,$who) 80]
    .c itemconfig player,$who -width 5 -outline $color
 }
 proc BMotion {who x y} {
    global S PLAYERS DIR2

    if {$S(turn) != $who} return
    if {$S(state) ne "move"} return

    foreach {row0 col0} $PLAYERS($who) break
    foreach {row1 col1} [Canvas2Tile [.c canvasx $x] [.c canvasy $y]] break
    set drow [expr {$row1-$row0}]
    set dcol [expr {$col1-$col0}]

    set drow [expr {$drow > 0 ? 1 : $drow < 0 ? -1 : 0}]
    set dcol [expr {$dcol > 0 ? 1 : $dcol < 0 ? -1 : 0}]
    if {$drow > 1 || $drow < -1}  return
    if {$dcol > 1 || $dcol < -1}  return
    if {$drow == 0 && $dcol == 0} return
    if {$drow != 0 && $dcol != 0} return
    set S(key) "mouse"
    MovePlayer $who $DIR2($drow,$dcol) 1
    set S(key) ""
 }
 proc BUp {who} {
    .c itemconfig player,$who -width 1 -outline $::COLORS(player,$who)
 }

 DoDisplay
 NewGame
 return

JM I could not see the "Done" button, looks like it is in the bottom of the GUI, just out of sight, and of reach )-:

KPV The "Done" button only appears after you've slide a tile. It appears in the same spot that the extra tile is located. The whole gui is based off the S(sz) and with it set to 100, the whole GUI is 922 pixels high. If that's too tall, just set that value top something smaller.

Brian Theado - Thanks for sharing this! My daughter and I have played this many times already and she loves it. My screen resolution is 1024x768 and a value of S(sz)=65 works well.

AvL Cute! Btw, one of the original rules (at least for the non-junior versions) is, that you must not do the previous move in the opposite direction. (you may still do it in the same direction). If this rule makes sense in the junior-version, then the arrow where previously a tile went out of the board would have to be "disabled".

...and btw., the stipple-pattern on each tile could be moved along with the tile using an "-offset".

KPV Added AvL's two suggestions. Started to add code to automatically config the size but the numbers weren't adding up and I gave up.

HJG v1.02: Changed the green of player 2 to have a bit more contrast against the background.