A small Ludo/Mensch ärgere Dich nicht/Pachisie game, originally developed by Richard Suchenwirth in plain Tcl (see http://wiki.tcl.tk/956). The game was rewritten as a design study in NX by Gustaf Neumann in July 2013.
Major changes:
-
object-oriented design (no global variables)
-
knowledge about the paths of the figures
-
animated moves
-
knowledge about the basic rules (e.g. need 6 to move out of the nest, have to move figures from starting position)
-
throw opponents out
-
sanity checks
-
user feedback
Short Instructions
-
The active player (marked with the button) has to dice (click on the die, or press somewhere on the board "d").
-
If all figures are in the nest (start position), the player needs to dice a 6. The player is allowed to try three times, then the player is done (press "done" button, or type "n") and the turn moves to the next player.
-
When a player got 6 eyes, he can move out of the nest. This is done by clicking on the figure the player wants to move.
-
After dicing 6, the player can dice again and move the player on the field (always by clicking on the figure).
Implementation
package require Tk package require nx::trait
Define an application specific converter "expr" that passes the scalar result of the expression. Since the converter is defined on nx::Slot, it is applicable to all method and configure arguments.
::nx::Slot method type=expr {name value} {return [expr $value]}
Class Figure
nx::Class create Figure { :property canvas:required :property x:double :property y:double :property size:double :property position:integer :property color :property no:integer :property board:object,required :variable tag "" :require trait nx::trait::callback :method init {} { # # Draw figure and define interactions # set d [expr {${:size}/6.}] set s [expr {${:size}/1.5}] set y [expr {${:y}-$d*2.5}] set :tag ${:color}${:no} set id [${:canvas} create arc [expr {${:x}-$s}] [expr {${:y}-$s}] \ [expr {${:x}+$s}] [expr {${:y}+$s}] -outline grey \ -start 250 -extent 40 -fill ${:color} \ -tags [list mv ${:tag}]] ${:canvas} create oval \ [expr {${:x}-$d}] [expr {${:y}-$d}] \ [expr {${:x}+$d}] [expr {${:y}+$d}] \ -fill ${:color} -outline grey -tags [list mv ${:tag}] #${:board} figure set $id [self] ${:canvas} bind ${:tag} <B1-ButtonRelease> [:callback go] } :public method go {} { # # Start moving the figure if the draw is permitted. # The board knows the die and the rules. # if {![${:board} moveFigure [self]]} { # stay at old position :gotoNr ${:position} } } :public method gotoNr {nr {-path ""} {-afterCmd ""}} { # # Move figure to the numbered position. If a path is given it # moves stepwise from position to position. # set oldPos ${:position} set :position $nr if {$path eq ""} {set path $nr} return [:move {*}[${:board} getPointCenter $oldPos] $path \ -afterCmd $afterCmd] } :protected method move {x0 y0 path:integer,1..n {-afterCmd ""}} { # # Move figure from old position (x0 y0) stepwise along the # path using animation. At the end of the move, 'afterCmd' is # issued. # set t 0 foreach pos $path { lassign [${:board} getPointCenter $pos] x y set stepx [expr {($x-$x0)/50.0}] set stepy [expr {($y-$y0)/50.0}] for {set i 0} {$i < 50} {incr i} { after [incr t 8] ${:canvas} move ${:tag} $stepx $stepy } lassign [list $x $y] x0 y0 incr t 100 } after $t ${:canvas} raise ${:tag} after $t $afterCmd set :x $x; set :y $y } :public object method lookup {position} { # # Return the figure at the provided position. This function # could be made faster, but is efficient enough as it is. # foreach f [Figure info instances] { if {[$f cget -position] == $position} { return $f } } return "" } }
Helper functions for the die
proc random:select L {lindex $L [expr int(rand()*[llength $L].)]} proc lexpr {term L} { # map an expr term to each element \$i of a list set res [list] foreach i $L {lappend res [eval expr $term]} set res }
Class Die
nx::Class create Die { :property canvas:required :property x:double :property y:double :property {size:double 25} :property {fg gold} :property {bg red} :property {eyes 0} :require trait nx::trait::callback :method set {n} { # # Set the eyes of the die. # ${:canvas} itemconfig ${:grouptag} -fill ${:bg} -outline ${:bg} foreach i [lindex [list \ {} {d5} [random:select {{d3 d7} {d1 d9}}] \ [random:select {{d1 d5 d9} {d3 d5 d7}}] \ {d1 d3 d7 d9} {d1 d3 d5 d7 d9} \ [random:select {{d1 d3 d4 d6 d7 d9} {d1 d2 d3 d7 d8 d9}}] \ ] $n] { ${:canvas} itemconfig ${:id}$i -fill ${:fg} -outline ${:fg} } set :eyes $n } :public method invalidate {} { # # Invalidate the eyes to avoid double uses of the eyes. # set :eyes 0 } :public method roll {} { # # Roll the dice and animate rolling # # wiggle: amount, pick one of eight wiggle directions set dwig [expr {${:size}/5}] for {set i 10} {$i<100} {incr i 10} { :set [expr {int(rand() * 6) + 1}] set wig [random:select {0,1 0,-1 1,0 -1,0 1,1 -1,1 1,-1 -1,-1}] set wig [lexpr \$i*$dwig [split $wig ,]] ${:canvas} move group${:id} {*}$wig update set wig [lexpr \$i*-1 $wig] ;# wiggle back ${:canvas} move group${:id} {*}$wig after $i } } :method init {} { # # initialize the widgets with tags interactions # set x [expr {${:x} - ${:size}/2.0}] set y [expr {${:y} - ${:size}/2.0}] set :id [${:canvas} create rect $x $y \ [expr {$x+${:size}}] [expr {$y+${:size}}] \ -fill ${:bg} -tags mvg] set :grouptag group${:id} ${:canvas} addtag ${:grouptag} withtag ${:id} set ex [expr {$x+${:size}/10.}] set ey [expr {$y+${:size}/10.}] set d [expr {${:size}/5.}];# dot diameter set dotno 1 ;# dot counter foreach y [list $ey [expr {$ey+$d*1.5}] [expr {$ey+$d*3}]] { foreach x [list $ex [expr {$ex+$d*1.5}] [expr {$ex+$d*3}]] { ${:canvas} create oval $x $y [expr {$x+$d}] [expr {$y+$d}] \ -fill ${:bg} -outline ${:bg} \ -tags [list mvg ${:grouptag} ${:id}d$dotno] incr dotno } } :set [expr {int(rand()*6)+1}] :invalidate # # To dice, let people click on the die, or press <d> on the # board # ${:canvas} bind mvg <1> [:callback roll] bind . <d> [:callback roll] } }
Class Board
nx::Class create Board { :property canvas:required :property {size:integer 25} :property {bg LightBlue1} :property {fg white} :property {colors:1..n {red green yellow blue}} :require trait nx::trait::callback :method lookup {var idx} { # # Convenience lookup function for arbitrary instance # variables. # set key "${var}($idx)" if {[info exists $key]} {return [set $key]} return "" } :public method getPointCenter {nr} {:lookup :pointCenter $nr} :public method getPointId {nr} {:lookup :pointId $nr} :method line { x0:expr,convert y0:expr,convert x1:expr,convert y1:expr,convert {-width 1} {-arrow none} } { # # Convenience function for line drawing, evaluates passed # expressions. # ${:canvas} create line $x0 $y0 $x1 $y1 -width $width -arrow $arrow } :method point {x:expr,convert y:expr,convert d {-number:switch false} -fill} { # # Draw a point (a position on the game board) and keep its # basic data in instance variables. We could as well turn the # positions into objects. # if {![info exists fill]} {set fill ${:fg}} incr :pointCounter set id [${:canvas} create oval \ [expr {$x-$d/2.}] [expr {$y-$d/2.}] \ [expr {$x+$d/2.}] [expr {$y+$d/2.}] \ -fill $fill -tags [list point] -outline brown -width 2] #${:canvas} create text $x $y -text ${:pointCounter} -fill grey set :pointNr($id) ${:pointCounter} set :pointCenter(${:pointCounter}) [list $x $y] set :pointId(${:pointCounter}) $id return ${:pointCounter} } :method fpoint {x:expr,convert y:expr,convert psize fsize color no} { # # Draw a point with a figure, note the position in the board # in the figure # set nr [:point $x $y $psize -fill $color] Figure new -board [self] -canvas ${:canvas} \ -x $x -y [expr {$y-$fsize/2.0}] \ -size $fsize -color $color -no $no -position $nr return $nr } :method pnest {x:expr,convert y:expr,convert d colorNr xf yf} { # # Draw the nest with the figures in it # set fsize [expr {$d/0.75}] set color [lindex ${:colors} $colorNr] lappend :nest($colorNr) [:fpoint $x-$d $y-$d $d $fsize $color 0] lappend :nest($colorNr) [:fpoint $x-$d $y+$d $d $fsize $color 1] lappend :nest($colorNr) [:fpoint $x+$d $y-$d $d $fsize $color 2] lappend :nest($colorNr) [:fpoint $x+$d $y+$d $d $fsize $color 3] set :buttonPos($colorNr) [list [expr $x+($xf*$d)] [expr $y+($yf*$d)]] } :method pline { x0:expr,convert y0:expr,convert x1:expr,convert y1:expr,convert d {-width 1} {-arrow none} } { # # Draw a path of the play-field with points (potential player # positions) on it. # set id [${:canvas} create line $x0 $y0 $x1 $y1 \ -width $width -arrow $arrow -fill brown] if {$x0 eq $x1} { # vertical set f [expr {$y1<$y0 ? -1.25 : 1.25}] for {set i 0} {$i < int(abs($y1-$y0)/($d*1.25))} {incr i} { :point $x0 $y0+$i*$d*$f $d } } else { # horizontal set f [expr {$x1<$x0 ? -1.25 : 1.25}] for {set i 0} {$i < int(abs($x1-$x0)/($d*1.25))} {incr i} { :point $x0+$i*$d*$f $y0 $d -number } } ${:canvas} lower $id } :method draw {m} { # # Draw board and create figures # set d ${:size} set u [expr {$d * 1.25}] # # Major positions: p0 .. p1 ..m.. p2 .. p3 # set p0 [expr {$u-$d/2.0}] set p1 [expr {$m-$u}] set p2 [expr {$m+$u}] set p3 [expr {2*$m-$u+$d/2}] :pline $p0 $p1 $p1 $p1 $d -width 4 :pline $p1 $p1 $p1 $p0 $d -width 4 :pline $p1 $p0 $p2 $p0 $d -width 4 ;# horizonal short line :pline $p2 $p0 $p2 $p1 $d -width 4 :pline $p2 $p1 $p3 $p1 $d -width 4 :pline $p3 $p1 $p3 $p2 $d -width 4 ;# vertical short line :pline $p3 $p2 $p2 $p2 $d -width 4 :pline $p2 $p2 $p2 $p3 $d -width 4 :pline $p2 $p3 $p1 $p3 $d -width 4 ;# horizonal short line :pline $p1 $p3 $p1 $p2 $d -width 4 :pline $p1 $p2 $p0 $p2 $d -width 4 :pline $p0 $p2 $p0 $p1 $d -width 4 ;# vertical short line :line $m+5*$d $m+2*$d $m+6*$d $m+2*$d -arrow first :line $m-2*$d $m+5*$d $m-2*$d $m+6*$d -arrow first :line $m-5*$d $m-2*$d $m-6*$d $m-2*$d -arrow first :line $m+2*$d $m-5*$d $m+2*$d $m-6*$d -arrow first set d2 [expr {$d*0.75}] set d15 $d2*2 set o [expr {$u*5}] :pnest $m+$o-$d $m-$o+$d $d2 0 -1 3 :pnest $m+$o-$d $m+$o-$d $d2 1 -1 -2.5 :pnest $d15 $m+$o-$d $d2 2 1 -2.5 :pnest $d15 $m-$o+$d $d2 3 1 3 for {set i 0; set y [expr $d*2]} {$i<4} {incr i;set y [expr {$y+$d}]} { lappend p(0) [:point $m $y $d2 -fill [lindex ${:colors} 0]] lappend p(1) [:point $m*2-$y $m $d2 -fill [lindex ${:colors} 1]] lappend p(2) [:point $m $m*2-$y $d2 -fill [lindex ${:colors} 2]] lappend p(3) [:point $y $m $d2 -fill [lindex ${:colors} 3]] } # # Setup the path per player and color the starting points # for {set i 1} {$i < 41} {incr i} {lappend path $i} foreach c {0 1 2 3} pos {11 21 31 1} o {11 21 31 1} { ${:canvas} itemconfig [:getPointId $pos] -fill [lindex ${:colors} $c] set :path($c) [concat [lrange $path $o-1 end] [lrange $path 0 $o-2] $p($c)] } } :public method msg {text} { # # Report a message to the user. # ${:canvas} itemconfig ${:msgId} -text $text return 0 } :public method wannaGo {obj pos {-path ""}} { # # We know that we can move the figure in principle. We have # to check, whether the target position is free. If the target # is occupied by our own player, we give up, otherwise we # through the opponent out. # if {$pos eq ""} {return [:msg "beyond path"]} set other [Figure lookup $pos] set afterCmd "" if {$other ne ""} { if {[$obj cget -color] eq [$other cget -color]} { # On player can't have two figure at the same place. return [:msg "My player is already at pos $pos"] } else { # Opponent is at the target position. Find a free # position in the opponents nest and though her out. set opponent [$other cget -color] foreach p [set :nest([lsearch ${:colors} $opponent])] { if {[Figure lookup $p] eq ""} { set afterCmd [list $other gotoNr $p] break } } } } :msg "[$obj cget -color]-[$obj cget -no] went to $pos" $obj gotoNr $pos -path $path -afterCmd $afterCmd ${:die} invalidate } :public method moveFigure {obj} { # # Move the provided figure by the diced eyes according to the # rules. First we check, if we are allowed to move this # figure, which might be in the nest or on the run. # set currentColor [lindex ${:colors} ${:player}] if {[$obj cget -color] ne $currentColor} { return [:msg "figure is not from the current player"] } set eyes [${:die} cget -eyes] if {$eyes == 0} { return [:msg "Must dice first"] } set position [$obj cget -position] if {$position in [set :nest(${:player})]} { # Figure is in the nest, just accept eyes == 6 if {$eyes == 6} { :wannaGo $obj [lindex [set :path(${:player})] 0] } else { return [:msg "Need 6 to move this figure"] } } else { # # Check, if we have still figures in the nest # set inNest "" foreach p [set :nest(${:player})] { set inNest [Figure lookup $p] if {$inNest ne ""} break } # # Check, if the actual figure is at the start position. # set startPos [lindex [set :path(${:player})] 0] set atStart [Figure lookup $startPos] if {$eyes == 6} { if {$inNest ne ""} { # Move a figure out from the nest, if we can if {$atStart ne ""} { if {[$atStart cget -color] eq $currentColor} { set path [set :path(${:player})] set current [lsearch $path $position] set targetPos [expr {$current + [${:die} cget -eyes]}] :wannaGo $obj [lindex $path $targetPos] \ -path [lrange $path $current+1 $targetPos] return 1 } } return [:msg "You have to move the figures from your nest first"] } } if {$atStart ne "" && $inNest ne "" && $obj ne $atStart} { return [:msg "You have to move the figures from the start first"] } set path [set :path(${:player})] set current [lsearch $path $position] set targetPos [expr {$current + [${:die} cget -eyes]}] :wannaGo $obj [lindex $path $targetPos] \ -path [lrange $path $current+1 $targetPos] } return 1 } :public method nextPlayer {} { # # Switch to the next player. # set :player [expr {(${:player}+1) % 4}] ${:canvas} coords ${:buttonWindow} {*}[set :buttonPos(${:player})] } :method init {} { set hw [expr {14 * ${:size}}] set center [expr {$hw / 2}] canvas ${:canvas} -bg ${:bg} -height $hw -width $hw :draw $center set :die [Die new -canvas ${:canvas} -x $center -y $center -size ${:size}] set :msgId [${:canvas} create text [expr {${:size}*4}] 10 -text ""] # # Player management (signal which player is next, etc.) # set :player 2 button .b1 -text "Done" -command [:callback nextPlayer] set :buttonWindow [.p create window 22 14 -window .b1] :nextPlayer bind . <n> [:callback nextPlayer] } }
Finally, create the board and pack it
Board new -canvas .p -bg beige -size 40
pack .p