Keith Vetter 2006-05-09 : Here's a version of the Black Box game, which, as wikipedia says [L1 ], is a game of hide and seek which simulates shooting rays into a black box to deduce the location of balls hidden inside. It was invented by Eric Solomon[L2 ].
One nice feature of this version is the ability to mark whole regions as not being possible ball locations. You do this by moving the mouse while holding down the right button.
I find that for the normal level of difficulty, a score over 20 is good.
lexfiend - Thanks, Keith! Brings back a lot of childhood memories. 8-)
LV (I hope firefox doesn't mangle the code on this page...) When I download this page, I'm not getting the left and right arrows displaying - instead, the unicode ascii representation is appearing. I wonder what font I need for those? The up and down arrows are appearing correctly.
KPV I'm using the unicode characters \u25bc, \u25b2, \u25ba and \u25c4 for the arrows. Perhaps your default font don't have these characters. On my Windows system, the font is Arial.
##+########################################################################## # # blackbox.tcl -- Plays the black box game # by Keith Vetter, May 2006 # package require Tk array set S {title "Black Box" iw 26 ih 26 lvl Normal Easy {6 6 3} Normal {8 8 4} Hard {10 10 7} fg yellow bg gray30 clr1 black clr2 gray40} foreach {S(w) S(h) S(n)} $S($S(lvl)) break image create photo ::img::atom -data { R0lGODlhGAAYALMAAPwODPyOjPxSVPw2PAT+BPyqrPxqbPwiJPyanPx+fPxCRAAAd9CgAAfqABYS AAAAACH5BAEAAAQALAAAAAAYABgAAwSvkMgpi70I0b3D/UWWcBuIWBkSrGSFhekaJDT5pZlM04ZR hjCVbMbrGQQTDyo33BmPAoUL1nT2BFiFYkC4CYnFa3RAHmCYzoR4PDgcAOfVTm2Mbttv+FIexo7d eXohOnR1AmVuAIpUYHVHWluJigAJfIVrd3iSYIV+CnaIgQAEl1B2mZqJEoZZqKmBE6ZaZW2vgBug oYC7byS0vAC7wS0EmpPHbwfEEpLHx8sbzqMtEQA7} image create photo ::img::badAtom -data { R0lGODlhGgAaALMAAAD/AISGhERCRMzOzCQiJKymrGRiZOzq7DQ2NFRWVJSWlLS2tNza3HRudPT2 9CQmJCH5BAEAAAAALAAAAAAaABoAAwTVEMhJn0hJPMq7fM3COOTBLA3hdc1IvqUzNOtUwPh7HEq9 5IfYbljw3HSw4HDZozRyDmV0yTAdDJOHKyncVa0nFeCJUy6HVYaagQX8kOZzes3oactTuUk9YAwW BAJBcHp0awMDCAlJcWlgfIiIF0BnXnR9kQKCJVRej35+iAsId3Foloehfyo3lXOGoJFNDZ5ge5eq iAkfC6ewfLGIBWIABp6/sbm7FAG/mMGRAUbAyZiRA00ezcDX3dI1xQWRucLL4AAEBgoLC6ILCgnE 5xMECJoI8h0RADs=} image create photo ::img::xAtom -data { R0lGODlhGAAYALMAAPwODAT+BPxSVPyOjPw2PPz+/PxqbPyqrPwiJPx+fPyanPxCRFCgABnqABYS AAAAACH5BAEAAAEALAAAAAAYABgAAwSwMIQi67lYqTqpLKA0YOShJV0IrqVyacowrHT9mrA8118N a7pEzdOjxXQDYU1QGQ2RSppAsJBghonskLogBEgK7LZLKGc0wyUZgQCcZWnQYt12v45RaZddt5uC aQJlBGwAhjgyWnF7fQAJiYpxBYR8hklZeXJDhYYAAQkGBkNzC5uFEqGjgwRDnRVDqwiEm681g3x8 Q0UrZbhtuTy2lZ2dbborEpzExClERBXLnhwhAREAOw==} image create photo ::img::cross -data { R0lGODlhGgAaALMAAAQCBFxaXHx+fGxubJSalCQiJIyOjGRiZISGhAwODHR6dISChHRydJyenCQm JGRmZCH5BAEAAAAALAAAAAAaABoAAwTX8MhJq73zSf26Pxq2HUBpnigQUtxRpGnxsd0wvHBZDN5K 1jZczMZ7ACiA3YDBUAhNBQUzeJzolgqFwHFyCLKMWylk1GUFiwUX4Eh/nSYkFL1AGBwBA2IhEDzJ KF52BgQGhntrJg8BICkOhgSRhXcpgI6RDQ2RiSeWKAGYmgQBMJ4mCZCShgmVG4F1qYcLTyquc2l2 eAh7fE9ySU19aiUJfG9ClgVMZ5xeYGJVEkkPS020SVJhYisqRNBDNh3RIDUP1ydKMxkd5ygyHRdl Oa0iIz3wPvX6IhEAOw==} image create photo ::img::blank -width $S(iw) -height $S(ih) ##+########################################################################## # # DoDisplay -- creates our gui # proc DoDisplay {} { global S font create boldFont -family Helvetica -size 10 -weight bold option add *highlightThickness 0 wm title . $S(title) wm resizable . 0 0 DoMenus frame .l -padx 10 -pady 10 -bg $S(bg) frame .c -padx 10 -pady 10 -bg $S(bg) #canvas .c -bg $S(bg) #. config -padx 10 -pady 10 -bg $S(bg) pack .l .c -side left -fill both label .l.title -text "Black\nBox" -font {Times 24 bold} -fg $S(fg) -bg $S(bg) label .l.lscore -text "Score:" -font {Times 12 italic} -fg $S(fg) -bg $S(bg) label .l.score -textvariable ::B(score) -font {Helvetica 24 bold italic} \ -fg $S(fg) -bg $S(bg) frame .l.atoms -bg $S(bg) label .l.reveal -text "Reveal?" -font {Times 12 bold} -fg $S(fg) -bg $S(bg) bind .l.reveal <1> Reveal label .l.again -text "Again?" -font {Times 12 bold} -fg $S(fg) -bg $S(bg) bind .l.again <1> NewGame pack .l.title -side top pack .l.lscore -side top -pady {20 0} pack .l.score -side top pack .l.atoms -side bottom -fill both DrawBoard FillAtoms bind all <Key-F1> Help bind all <Key-F2> NewGame bind all <Key-F3> {console show} } ##+########################################################################## # # DoMenus -- isn't installing menus really verbose and clunky? # proc DoMenus {} { option add *Menu.tearOff 0 . config -menu [menu .menu] menu .menu.game .menu add cascade -label "Game" -underline 0 -menu .menu.game .menu.game add command -label "New Game" -command NewGame -underline 0 \ -accelerator "F2" .menu.game add separator .menu.game add cascade -label "Level" -menu .menu.game.lvl -underline 0 menu .menu.game.lvl foreach lev [list "Easy" "Normal" "Hard"] { .menu.game.lvl add radio -label "$lev" \ -variable ::S(lvl) \ -value $lev \ -underline 0 \ -command Resize } .menu.game add separator .menu.game add command -label "Exit" -underline 1 -command exit menu .menu.help .menu add cascade -label "Help" -underline 0 -menu .menu.help .menu.help add command -label "Help" -underline 0 -command Help \ -accelerator "F1" .menu.help add command -label "About" -underline 0 -command About } proc Resize {} { global S foreach {S(w) S(h) S(n)} $S($S(lvl)) break DrawBoard FillAtoms NewGame } ##+########################################################################## # # FillAtoms -- creates GUI which shows how many atoms are left to place # proc FillAtoms {} { global S eval destroy [winfo child .l.atoms] set row 0 set col 0 for {set i 0} {$i < $S(n)} {incr i} { label .l.atoms.$i -image ::img::atom -width $S(iw) -height $S(ih) \ -bg $S(bg) grid .l.atoms.$i -row $row -column $col if {[incr col] == 2} { incr row set col 0 } } } ##+########################################################################## # # UpdateAtoms -- updates GUI to show how many more atoms need placing # proc UpdateAtoms {} { global S B set num [llength $B(where)] for {set i 0} {$i < $S(n)} {incr i} { set img [expr {$i < $num ? "::img::blank" : "::img::atom"}] .l.atoms.$i config -image $img } place forget .l.reveal place forget .l.again if {$num == $S(n)} { place .l.reveal -relx .5 -rely 1 -anchor s } } proc DrawBoard {} { global S eval destroy [winfo child .c] set S(w1) [expr {$S(w)+1}] set S(h1) [expr {$S(h)+1}] set S(w2) [expr {$S(w)+2}] set S(h2) [expr {$S(h)+2}] for {set row 0} {$row < $S(h2)} {incr row} { grid rowconfigure .c $row -pad 0 for {set col 0} {$col < $S(w2)} {incr col} { grid columnconfigure .c $col -pad 0 if {$row == 0 || $row > $S(h) || $col == 0 || $col > $S(w)} { if {($row == 0 || $row > $S(h)) && ($col == 0 || $col > $S(w))} continue label .c.g$row,$col -image ::img::blank \ -width $S(iw) -height $S(ih) \ -bg $S(clr2) -relief raised -bd 2 \ -compound center -font boldFont bind .c.g$row,$col <1> [list Ray $row $col] grid .c.g$row,$col -row $row -column $col } else { label .c.b,$row,$col -image ::img::blank \ -width $S(iw) -height $S(ih) \ -bg $S(clr1) -relief raised -bd 2 bind .c.b,$row,$col <1> [list Click $row $col] bind .c.b,$row,$col <ButtonPress-3> [list RClick down $row $col] bind .c.b,$row,$col <B3-Motion> [list RClick move %X %Y] grid .c.b,$row,$col -row $row -column $col -sticky news } } } grid rowconfigure .c [list 0 $S(h1)] -pad 10 grid columnconfigure .c [list 0 $S(w1)] -pad 10 } ##+########################################################################## # # Reset -- resets all data structures and GUI # proc Reset {} { global B S array unset B set B(where) {} set B(atoms) {} set B(rays) 0 set B(ray,id) 0 set B(score,base) [expr {2*($S(w)+$S(h))}] set B(score) "[expr {$B(score,base)-$B(rays)}]-?" # Reset board data and board display for {set row 1} {$row <= $S(h)} {incr row} { for {set col 1} {$col <= $S(w)} {incr col} { set B(b,$row,$col) 0 .c.b,$row,$col config -image ::img::blank -bg $S(clr1) } } # Reset arrow buttons foreach row [list 0 [expr {$S(h)+1}]] { set ch [expr {$row == 0 ? "\u25bc" : "\u25b2"}] for {set col 1} {$col <= $S(w)} {incr col} { set B(r,$row,$col) 0 .c.g$row,$col config -text $ch -fg black } } foreach col [list 0 [expr {$S(w)+1}]] { set ch [expr {$col == 0 ? "\u25ba" : "\u25c4"}] for {set row 1} {$row <= $S(h)} {incr row} { set B(r,$row,$col) 0 .c.g$row,$col config -text $ch -fg black } } UpdateAtoms } ##+########################################################################## # # Click -- handles clicking on the grid to place an atom # proc Click {row col} { global B S if {$B(state) ne "play"} return set cell "b,$row,$col" if {$B($cell) & 2} { ;# Already placed an atom set B($cell) [expr {$B($cell) & 1}] ;# Clear .c.$cell config -image ::img::blank set n [lsearch $B(where) $cell] set B(where) [lreplace $B(where) $n $n] } else { ;# Empty location if {[llength $B(where)] < $S(n)} { set B($cell) [expr {($B($cell) & 1) | 2}] ;# Clear and set .c.$cell config -image ::img::atom lappend B(where) $cell } } UpdateAtoms } ##+########################################################################## # # _RClick -- handles toggling the X in square row,col # proc _RClick {row col} { global B S if {$B(state) ne "play"} return set cell "b,$row,$col" if {$B($cell) & 4} { ;# User cross set B($cell) [expr {$B($cell) & 1}] ;# Clear .c.$cell config -image ::img::blank } else { if {$B($cell) & 2} { ;# Was there an atom there? set n [lsearch $B(where) $cell] set B(where) [lreplace $B(where) $n $n] } set B($cell) [expr {($B($cell) & 1) | 4}] ;# Clear and set .c.$cell config -image ::img::cross } UpdateAtoms } ##+########################################################################## # # RClick -- handles right click and possible sweeping motion # proc RClick {how r c} { global B if {$B(state) ne "play"} return if {$how eq "down"} { set cell "b,$r,$c" set B(onoff) [expr {! ($B($cell) & 4)}] ;# How we want the cell to be _RClick $r $c } elseif {$how eq "move"} { set w [winfo containing $r $c] if {! [winfo exists $w]} { puts "move: ?" return } scan $w ".c.b,%d,%d" r c set cell "b,$r,$c" if {! [info exists B($cell)]} return set isX [expr {$B($cell) & 4}] if {$B(onoff) && ! $isX} { _RClick $r $c } if {! $B(onoff) && $isX} { _RClick $r $c } } } ##+########################################################################## # # Ray -- Handles firing a ray into the black box # proc Ray {row col} { global B S if {$B(state) ne "play"} return if {$B(r,$row,$col) != 0} return ;# Already fired set drow [expr {$row == 0 ? 1 : $row > $S(h) ? -1 : 0}] set dcol [expr {$col == 0 ? 1 : $col > $S(w) ? -1 : 0}] set what [ShootRay $row $col $drow $dcol] if {$what eq "A" || $what eq "R"} { .c.g$row,$col config -image ::img::blank -text $what -fg $S(fg) set B(r,$row,$col) $what incr B(rays) } else { foreach {r c} $what break set B(r,$row,$col) [incr B(ray,id)] set B(r,$r,$c) $B(ray,id) .c.g$row,$col config -image ::img::blank -text $B(ray,id) -fg $S(fg) .c.g$r,$c config -image ::img::blank -text $B(ray,id) -fg $S(fg) incr B(rays) 2 } set B(score) "[expr {$B(score,base)-$B(rays)}]-?" } ##+########################################################################## # # ShootRay -- does the actual ray tracing # proc ShootRay {row col drow dcol} { global B S set B(path) [list $row $col] while {1} { set r [expr {$row + $drow}] ;# Next position set c [expr {$col + $dcol}] lappend B(path) $r $c if {[OffBoard $r $c]} {return [list $r $c]} ;# Did we exit??? if {$B(b,$r,$c) & 1} { return "A" } ;# Did we hit something set r1 [expr {$r - abs($dcol)}] ;# Check for detours set r2 [expr {$r + abs($dcol)}] set c1 [expr {$c - abs($drow)}] set c2 [expr {$c + abs($drow)}] set corner1 [expr {! [OffBoard $r1 $c1] && ($B(b,$r1,$c1) & 1)}] set corner2 [expr {! [OffBoard $r2 $c2] && ($B(b,$r2,$c2) & 1)}] if {! $corner1 && ! $corner2} { ;# Missed foreach row $r col $c break ;# Move forward continue } if {$corner1 && $corner2} { return "R" };# Double hit if {[OffBoard $row $col]} { return "R" } ;# Edge corner hit # Turn a corner set B(path) [lrange $B(path) 0 end-2] set tmp [expr {$corner1 ? abs($dcol) : -abs($dcol)}] set dcol [expr {$corner1 ? abs($drow) : -abs($drow)}] set drow $tmp } } proc Path2XY {} { global B S set xy {} foreach {r c} $B(path) { set cell .c.b,$r,$c if {[OffBoard $r $c]} { set cell .c.g$r,$c} set x [expr {[winfo x $cell] + $S(iw)/2}] set y [expr {[winfo y $cell] + $S(ih)/2}] lappend xy $x $y } return $xy } proc OffBoard {row col} { return [expr {$row == 0 || $row > $::S(h) || $col == 0 || $col > $::S(w)}] } ##+########################################################################## # # PlaceAtoms -- hides cnt atoms in our black box # proc PlaceAtoms {cnt} { global B set B(atoms) {} set all [array names B b,*] while {$cnt} { set n [expr {int(rand() * [llength $all])}] set cell [lindex $all $n] set B($cell) 1 lappend B(atoms) $cell set all [lreplace $all $n $n] incr cnt -1 } } ##+########################################################################## # # Reveal -- show where the atoms are hidden # proc Reveal {} { global B S if {$B(state) ne "play"} return # good guessed => yellow bg # bad guess => xAtom image # missing guess => badAtom image set B(state) done set misses 0 foreach cell $B(atoms) { if {[lsearch $B(where) $cell] != -1} { ;# Correctly found .c.$cell config -bg $S(fg) } else { .c.$cell config -image ::img::badAtom incr misses 5 } } foreach cell $B(where) { if {[lsearch $B(atoms) $cell] != -1} continue ;# Correctly found .c.$cell config -image ::img::xAtom } place forget .l.reveal place .l.again -relx .5 -rely 1 -anchor s set B(score) [expr {$B(score,base)-$B(rays)-$misses}] } ##+########################################################################## # # NewGame -- starts a new game # proc NewGame {} { global B S Reset set B(state) "play" PlaceAtoms $S(n) set B(state) play } ##+########################################################################## # # About -- tell something about us # proc About {} { set txt "$::S(title)\n\nby Keith Vetter\nMay, 2006" tk_messageBox -icon info -message $txt -title "About $::S(title)" } ##+########################################################################## # # Help -- a simple help screen # proc Help {} { catch {destroy .help} toplevel .help wm title .help "$::S(title) Help" #wm geom .help "+[expr {[winfo x .] + [winfo width .] + 10}]+[winfo y .]" set t .help.t text $t -relief raised -wrap word -width 70 -height 31 \ -padx 10 -pady 10 -cursor {} -yscrollcommand {.help.sb set} scrollbar .help.sb -orient vertical -command [list $t yview] button .help.dismiss -text Dismiss -command {destroy .help} pack .help.dismiss -side bottom -pady 10 pack .help.sb -side right -fill y 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 "Times 20 bold" $t tag configure title2 -justify center -font "Times 12 bold" $t tag configure header -font $bold -spacing3 5 $t tag configure bold -font $bold $t tag configure italic -font $italic $t tag configure n -lmargin1 10 -lmargin2 10 $t tag configure bullet -lmargin1 20 -lmargin2 30 $t insert end "$::S(title)\n" title "by Keith Vetter\n\n" title2 set txt "$::S(title) is a game of \"hide and seek\" which simulates " append txt "shooting electron rays into a black box to try and deduce " append txt "the locations of various atoms hidden inside. It was " append txt "invented by Eric Solomon " append txt "(http://www.ericsolomon.co.uk/).\n\n" $t insert end "Introduction\n" header $txt set txt "Each ray fired into the black box reveals some " append txt "information about the location of the hidden atoms." append txt "The rays interact with the atoms in three ways.\n\n" $t insert end "Rules\n" header $txt set txt "A ray which directly hits an atom is absorbed " append txt "and doesn't emerge from the box. This is marked by an \"A\"\n" $t insert end \u25cf bullet " Absorption: " bold $txt bullet set txt "A ray which passes directly to the side " append txt "of one atom is deflected by 90 degrees before continuing on. " append txt "This is marked by labelling the entry and exit points " append txt "with the same id.\n" $t insert end \u25cf bullet " Deflection: " bold $txt bullet set txt "A reflection can occur in two ways, either by " append txt "a ray being deflected twice simultaneously, or " append txt "by a ray aimed directly beside an atom located at the edge " append txt "of the grid. This is marked by an \"R\".\n\n" $t insert end \u25cf bullet " Reflection: " bold $txt bullet set txt "More complex paths can occur when a ray is deflected one " append txt "or more times before being absorbed, reflected or exiting " append txt "the grid.\n\n" $t insert end $txt $t insert end "How To Play\n" header set txt "\u25cf To fire an electron ray, click on edge square.\n" $t insert end $txt bullet set txt "\u25cf To place an atom in the box, click on any square " append txt "in the box. To remove it, click it again.\n" $t insert end $txt bullet set txt "\u25cf To X out a square, right-click on the square. " append txt "To remove it, right-click it again.\n" $t insert end $txt bullet set txt "\u25cf To X out multiple square, hold down the right button " append txt "and sweep out the area to X out. Repeating will clear it.\n\n" $t insert end $txt bullet set txt "Your score starts off with the number of possible rays. " append txt "You lose one point for every " append txt "ray entry and exit. You lose five points for every wrong " append txt "guess about an atom's location. Thus five rays are equal " append txt "to one missed atom." $t insert end "Scoring\n" header $txt $t config -state disabled } ################################################################ DoDisplay NewGame return
gold added pix