Keith Vetter 2006-02-09 : Here's a fun litle game where you have to rotate nodes to connect up wires to light up every node. I wrote a somewhat similar game earlier called Lights Out.
Fun project for several reasons: 1) implemented a minimum spanning tree algorithm (Prim's) 2) implemented a depth-first search and 3) did a poor man's anti-aliasing to get nicer looking lines.
ABU Really nice. Just for the blockheads like me, could you provide a "solve" command with a slow animation ?
KPV There's already a built it cheat command. Just hold the control key down and click on a node. That will cause that node to orient itself correctly.
GS 2010-10-23 : A slightly modified version [L1 ] for touchscreen Windows Mobile device with eTcl. Lights bulbs are larger and expert mod has been disabled because it is too larger to fit in smartphone screen resolution.
Jeff Smith 2019-04-23 : Below is an online demo using CloudTk
uniquename 2013aug01
Here is a right-clipped image of the 'desktop' version --- along with a (partly-clipped) image of the help popup.
This is the initial setting of the GUI. The little rods rotate to 'hexagonal-angles' --- and lights come on as more segments are attached, to complete the circuit to the central light.
##+########################################################################## # # lightsOn.tcl -- based on http://pyva.net/eng/pc/lights.html # by Keith Vetter # package require Tk set G(n) 7 array set S {title "Lights On" w 600 h 600 vdist 50 hdist 28} array set DRC {0 {0 2} 1 {-1 1} 2 {-1 -1} 3 {0 -2} 4 {1 -1} 5 {1 1}} array set COLORS {ray1 \#4C526C ray2 \#8C96B4} proc DoDisplay {} { global S wm title . $S(title) canvas .c -bg black -width $S(w) -height $S(h) -highlightthickness 0 label .t -textvariable ::G(tmsg) -font {Times 18 bold} \ -fg cyan -bg black -anchor w -padx 10 pack .t -side top -fill x pack .c -side top -fill both -expand 1 bind all <F2> NewGame bind all <F3> {console show} DoMenus bind .c <Configure> {ReCenter %W %h %w} ;# Force 0,0 to be in center update } proc DoMenus {} { menu .m -tearoff 0 . configure -menu .m ;# Attach menu to main window .m add cascade -menu .m.game -label "Game" -underline 0 .m add cascade -menu .m.help -label "Help" -underline 0 menu .m.game -tearoff 0 .m.game add command -label "New Game" -under 0 -command NewGame -acc "F2" .m.game add separator .m.game add radiobutton -label "Easy" -under 0 -variable ::G(n) -value 5 -command NewGame .m.game add radiobutton -label "Normal" -under 0 -variable ::G(n) -value 7 -command NewGame .m.game add radiobutton -label "Hard" -under 0 -variable ::G(n) -value 9 -command NewGame .m.game add radiobutton -label "Expert" -under 0 -variable ::G(n) -value 11 -command NewGame .m.game add separator .m.game add command -label "Exit" -under 1 -command exit menu .m.help -tearoff 0 .m.help add command -label "About" -under 0 -command About } proc DrawBoard {} { global B .c delete all for {set row $::G(-n2)} {$row < $::G(n2)} {incr row} { set cmax [expr {$::G(n)-1-abs($row)}] for {set col -$cmax} {$col <= $cmax} {incr col 2} { DrawCellRays $row $col .c create image [Cell2XY $row $col] -image ::img::ball \ -tag b$row,$col .c bind b$row,$col <1> [list Click $row $col 1] .c bind r$row,$col <1> [list Click $row $col 1] .c bind b$row,$col <3> [list Click $row $col -1] .c bind r$row,$col <3> [list Click $row $col -1] .c bind b$row,$col <Control-1> [list Cheat $row $col] .c bind r$row,$col <Control-1> [list Cheat $row $col] } } LightUp } proc DrawRay {row col dir} { foreach {r1 c1} [MoveDir $row $col $dir] break foreach {x0 y0} [Cell2XY $row $col] break foreach {x1 y1} [Cell2XY $r1 $c1] break set x2 [expr {$x0 + ($x1-$x0)/2}] ;# Halfway point set y2 [expr {$y0 + ($y1-$y0)/2}] set tag r$row,$col .c create line $x0 $y0 $x2 $y2 -tag [list r1 $tag] -width 4 -fil $::COLORS(ray1) .c create line $x0 $y0 $x2 $y2 -tag [list r2 $tag] -width 2 -fil $::COLORS(ray2) .c lower r$row,$col } proc DrawCellRays {row col} { .c delete r$row,$col foreach dir $::B(r,$row,$col) { DrawRay $row $col $dir } } proc Cell2XY {row col} { set x [expr {$col * $::S(hdist)}] set y [expr {-$row * $::S(vdist)}] return [list $x $y] } ##+########################################################################## # # Recenter -- keeps 0,0 at the center of the canvas during resizing # 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 Timer {{restart 0}} { global G foreach aid [after info] { after cancel $aid } if {$restart} { set G(start) [clock seconds] } if {$G(state) ne "play"} return set tlen [expr {[clock seconds] - $G(start)}] set G(tmsg) [clock format $tlen -format "%M:%S"] after 1000 Timer } proc NewGame {} { MakeBoard DrawBoard set ::G(state) play Timer 1 } ##+########################################################################## # # MakeBoard -- figures out all the nodes and all edges, then deletes # edges leaving a minimum spanning tree and finally randomly rotates # all nodes. # proc MakeBoard {} { global B G EDGES unset -nocomplain B set EDGES {} set G(n2) [expr {($G(n)+1)/2}] ;# Handy constants set G(-n2) [expr {1-$G(n2)}] for {set row $::G(-n2)} {$row < $::G(n2)} {incr row} { set cmax [expr {$::G(n)-1-abs($row)}] for {set col -$cmax} {$col <= $cmax} {incr col 2} { set B(c,$row,$col) 1 } } # Compute all legal edges for {set row $::G(-n2)} {$row < $::G(n2)} {incr row} { set cmax [expr {$::G(n)-1-abs($row)}] for {set col -$cmax} {$col <= $cmax} {incr col 2} { set B(r,$row,$col) [FindNeighbors $row $col] foreach dir {0 1 2} { if {[lsearch $B(r,$row,$col) $dir] > -1} { lappend EDGES [list $row $col $dir] } } } } set G(cnt) [llength [array names B c*]] # Now convert full graph into minimum spanning tree set mst [MST] foreach e $EDGES { if {[lsearch $mst $e] == -1} { ;# Is edge not in MST??? eval RemoveEdge $e ;# ...then remove it } } # Now rotate randomly every node for {set row $::G(-n2)} {$row < $::G(n2)} {incr row} { set cmax [expr {$::G(n)-1-abs($row)}] for {set col -$cmax} {$col <= $cmax} {incr col 2} { set B(rr,$row,$col) $B(r,$row,$col) RotateCell $row $col [expr {int(rand()*6)}] } } } ##+########################################################################## # # FindNeighbors -- returns list of all legal directions from this node # proc FindNeighbors {row col} { global B set dirs {} foreach dir {0 1 2 3 4 5} { foreach {r c} [MoveDir $row $col $dir] break if {[info exists B(c,$r,$c)]} { lappend dirs $dir } } return $dirs } proc MoveDir {row col dir} { foreach {dr dc} $::DRC($dir) break set r1 [expr {$row + $dr}] set c1 [expr {$col + $dc}] return [list $r1 $c1] } ##+########################################################################## # # Click -- handles clicking on a node # proc Click {row col rdir} { RotateCell $row $col $rdir DrawCellRays $row $col LightUp } proc Cheat {row col} { set ::B(r,$row,$col) $::B(rr,$row,$col) DrawCellRays $row $col LightUp } proc RotateCell {row col rdir} { global B set dirs {} foreach dir $B(r,$row,$col) { lappend dirs [expr {($dir + $rdir) % 6}] } set B(r,$row,$col) $dirs } ##+########################################################################## # # LightUp -- does a depth-first-search to find all connected components # proc LightUp {} { global DFS DFS set solved 1 for {set row $::G(-n2)} {$row < $::G(n2)} {incr row} { set cmax [expr {$::G(n)-1-abs($row)}] for {set col -$cmax} {$col <= $cmax} {incr col 2} { set img "::img::ball2" if {! $DFS($row,$col)} { set solved 0 set img "::img::ball" } .c itemconfig b$row,$col -image $img } } if {$solved} Victory } proc Victory {} { global G if {$G(state) ne "play"} return set G(state) solved Flash } proc Flash {{cnt 3} {delay 200}} { for {set i 0} {$i < $cnt} {incr i} { .c config -bg red .t config -bg red update after $delay .c config -bg black .t config -bg black update after $delay } } ##+########################################################################## # # RemoveEdge -- removes an edge for a given node and the reverse node # proc RemoveEdge {row col dir} { global B foreach {r c} [MoveDir $row $col $dir] break set opp [expr {($dir + 3) % 6}] set n [lsearch $B(r,$row,$col) $dir] set B(r,$row,$col) [lreplace $B(r,$row,$col) $n $n] set n [lsearch $B(r,$r,$c) $opp] set B(r,$r,$c) [lreplace $B(r,$r,$c) $n $n] } ##+########################################################################## # # DFS -- does a depth-first-search from the origin. This can blow out # the recursion limit for big board sizes. # proc DFS {} { global DFS unset -nocomplain DFS set DFS(cnt) 0 for {set row $::G(-n2)} {$row < $::G(n2)} {incr row} { set cmax [expr {$::G(n)-1-abs($row)}] for {set col -$cmax} {$col <= $cmax} {incr col 2} { set DFS($row,$col) 0 } } _DFS 0 0 } ##+########################################################################## # # _DFS -- recursive caller for DFS # proc _DFS {row col} { global B DFS set DFS($row,$col) 1 incr DFS(cnt) foreach dir $B(r,$row,$col) { if {! [IsPath $row $col $dir]} continue foreach {r c} [MoveDir $row $col $dir] break if {$DFS($r,$c) != 0} continue _DFS $r $c } } ##+########################################################################## # # IsPath -- return true if there is a path from row,col in direction dir # assumes path exists out of row,col so it checks for off the board # and is there a matching opposite path from destination # proc IsPath {row col dir} { global B foreach {r c} [MoveDir $row $col $dir] break if {! [info exists B(c,$r,$c)]} { return 0 };# Destination off the board set opp [expr {($dir + 3) % 6}] set n [lsearch $B(r,$r,$c) $opp] return [expr {$n > -1 ? 1 : 0}] } 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 About {} { set txt "$::S(title)\nby Keith Vetter, February 2006\n\n" append txt "Turn on all the lights!\n\n" append txt "Left click: rotate clockwise\n" append txt "Right click: rotate counter-clockwise\n" tk_messageBox -message $txt -title About } ##+########################################################################## # # MST -- computes a random minimum spanning tree using Prim's algorithm # proc MST {} { global B EDGES set mst {} # Mark all nodes as unvisited for {set row $::G(-n2)} {$row < $::G(n2)} {incr row} { set cmax [expr {$::G(n)-1-abs($row)}] for {set col -$cmax} {$col <= $cmax} {incr col 2} { set visited($row,$col) 0 } } set edges [Shuffle $EDGES] foreach {r c} [lindex $edges 0] break ;# Start with random node set visited($r,$c) 1 while {[llength $mst] < $::G(cnt)-1} { # Find edge out of visited nodes (inefficient but who cares) for {set i 0} {$i < [llength $edges]} {incr i} { foreach {r0 c0 dir} [lindex $edges $i] break ;# Start point foreach {r1 c1} [MoveDir $r0 $c0 $dir] break ;# End point if {$visited($r0,$c0) != $visited($r1,$c1)} break } set edges [lreplace $edges $i $i] ;# Remove from edge list lappend mst [list $r0 $c0 $dir] ;# Add to our mst set visited($r0,$c0) 1 ;# Mark nodes as visited set visited($r1,$c1) 1 } return $mst } ################################################################ image create photo ::img::ball -data { R0lGODlhCgAKALMAACQmJHd3d6SmpAQC/ExKTJGRkcTCxFxeXDk3Oby6vISGhJyenNDQ0GZnZlRW VKyurCH5BAEAAAMALAAAAAAKAAoAAwQ5cIyWGHtOjmJNekszHN4jLIUSIAWYBkHTEEUBN0d+ODHu /I4dIQgMHggAo3EHGCB+uR9CAxg6kJIIADs=} image create photo ::img::ball2 -data { R0lGODlhCgAKALMAAGRXBJSKBN7MBOzYXLyyBOjkBMq/BN3XBAQC/Pz2fJR6BJSWBPPwBL6aBGRm BN23BCH5BAEAAAgALAAAAAAKAAoAAwQzECFnzjFAomWtEMFkFQxTCBghkKX5NMaQzPTwPDI9283D loUX4MYKPjKKm/Kh0AB6QkkEADs=} DoDisplay NewGame return
Lights On is available for Android on Playstore [L2 ]