Keith Vetter 2004-09-10 : This is a one-person puzzle in which you try to turn off all the lights. The board consists of a lattice of lights which can be turned on and off. Clicking on any light toggles the on/off state of that light and its four vertically and horizontally adjacent neighbors.
Determining if a given random arrangement of on/off lights can be all turned off is known as the All-Ones Problem.
One interesting quirk is that it doesn't matter the order of toggling lights needed to reach the solution--the solution is commutative.
KPV 2004-09-11 : Added a "Hint" button which first does a search find the solution then displays one of the lights which needs to be toggled. The solver does a BFS and can be time consuming for large size boards.
HJG 2014-05-08 : I added buttons to cycle thru some canned puzzles, changed the board to be drawn in alternate colors based on boardsize, and disabled the solver for big gameboards to prevent it from crashing (with an exception for the stored puzzles).
The solver can find some solutions for 5x5 in reasonable time, but 4x5 and maybe 7x3 is the maximum that can be handled safely.
Also, proc InitBoard currently just puts random values into each cell. For some boardsizes (e.g. 4x4), this results in many unsolvable games.
Maybe an alternative approach would work better, e.g. starting with the empty board, then doing some random moves...
uniquename 2013aug01 added pix
##+########################################################################## # # LightsOut.tcl - description # by Keith Vetter -- Sept 10, 2004 # http://mathworld.wolfram.com/LightsOutPuzzle.html # # Collection of puzzles, and solver-limiting # 2014-05 by HaJo Gurt # package require Tk array set S {title "Lights Out" version "v1.14 2014-05-08" w 500 h 500} array set G {rows 3 cols 3 maxSolve 21 puz 70 autoSolve 0} global T1 T2 proc DoDisplay {} { wm title . "$::S(title) $::S(version)" frame .ctrl -relief ridge -bd 2 -padx 5 -pady 5 canvas .c -relief raised -height $::S(h) -width $::S(w) -bd 2 \ -highlightthickness 0 -relief raised pack .ctrl -side right -fill both -ipady 5 pack .c -side top -fill both -expand 1 bind all <Escape> {exit} bind all <Key-F2> {console show} bind .c <Configure> {ReCenter %W %h %w} DoCtrlFrame } 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] DrawBoard 1 } proc Puzzle { dir } { #: Select puzzle global G S incr G(puz) $dir if { $G(puz) < 0 } { set G(puz) 70 } ;# TODO set puzz {} # 1 2 3 # 1 2 3 4 # 1 2 3 4 5 # 4 5 6 # 5 6 7 8 # 6 7 8 9 10 # 7 8 9 # 9 10 11 12 # 11 12 13 14 15 # 10 11 12 # 13 14 15 16 # 16 17 18 19 20 # 13 14 15 # 17 18 19 20 # 21 22 23 24 25 # ... # 16 17 18 # 64 65 66 67 68 69 70 ## ## 1 { set puzz { 5 4 0 3 4 8 9 10 14 15 } ;# ct-hard : 136s / 10 moves } ## switch $G(puz) { 1 { set puzz { 3 3 0 1 } ;# 5 moves } 2 { set puzz { 3 3 0 2 } ;# 4 moves } 3 { set puzz { 3 3 0 5 } ;# c 5 moves } 4 { set puzz { 3 3 0 4 5 } ;# -. 5 moves } 5 { set puzz { 3 3 0 4 6 } ;# .. 6 moves } 6 { set puzz { 3 3 0 7 8 } ;# _. 5 moves } 7 { set puzz { 3 3 0 7 9 } ;# ._. 4 moves } 8 { set puzz { 3 3 0 7 6 } ;# 5 moves } 9 { set puzz { 3 3 0 8 6 } ;# 4 moves } 10 { set puzz { 3 3 0 4 6 8 } ;# -_- 6 moves } 11 { set puzz { 3 3 0 4 5 6 } ;# --- 7 moves } 12 { set puzz { 3 3 0 1 3 5 } ;# v 5 moves } 13 { set puzz { 3 3 0 1 3 8 } ;# V 8 moves } 14 { set puzz { 3 3 0 1 3 9 } ;# 3 moves } 15 { set puzz { 3 3 -1 2 4 6 8 } ;# 9 moves } 16 { set puzz { 4 3 0 10 } ;# 7 moves } 17 { set puzz { 4 3 0 11 } ;# 7 moves } 18 { set puzz { 4 3 0 7 } ;# 7 moves } 19 { set puzz { 4 3 0 8 } ;# 4 moves } 20 { set puzz { 4 3 0 10 12 } ;# ":" 6 moves } 21 { set puzz { 4 3 0 11 12 } ;# "-" 6 moves } 22 { set puzz { 4 3 0 9 11 } ;# "-" 6 moves } 23 { set puzz { 4 3 0 9 12 } ;# 6 moves } 24 { set puzz { 4 3 0 6 12 } ;# 6 moves } 25 { set puzz { 4 3 0 3 12 } ;# 10 moves } 26 { set puzz { 4 3 0 2 9 } ;# ",-" 6 moves } 27 { set puzz { 4 3 0 2 11 } ;# ":" 10 moves } 28 { set puzz { 4 3 0 2 8 } ;# : 5 moves } 29 { set puzz { 4 3 0 2 8 11 } ;# "i" 8 moves } 30 { set puzz { 4 3 0 4 6 8 10 12 } ;# "x" 6 moves } 31 { set puzz { 4 3 0 2 4 6 8 10 12 } ;# "//" 7 moves } 32 { set puzz { 4 3 0 5 10 11 12 } ;# 7 moves } 33 { set puzz { 4 3 -1 1 3 5 11 } ;# "A" 3 moves } 34 { set puzz { 4 3 0 3 6 10 11 12 } ;# _' 5 moves } 35 { set puzz { 4 3 0 1 2 3 8 11 } ;# 6 moves } 36 { set puzz { 4 3 0 1 2 3 5 8 11 } ;# "T" 8 moves } 37 { set puzz { 4 4 -1 0 } ;# "@" 4 moves / 0 s } 38 { set puzz { 4 4 0 1 4 13 16 } ;# "4Q" 6 moves / 1 s } 39 { set puzz { 4 4 -1 1 4 13 16 } ;# "4+" 4 moves / 0 s } 40 { set puzz { 4 4 0 6 7 9 12 14 15 } ;# "<>" 2 moves / 1 s } 41 { set puzz { 4 4 -1 6 7 9 12 14 15 } ;# "<>" 4 moves / 1 s } 42 { set puzz { 4 4 -1 6 7 10 11 } ;# "O" 4 moves / 0 s } 43 { set puzz { 4 4 0 6 7 10 11 } ;# "o" 6 moves / 1 s } 44 { set puzz { 4 4 0 4 7 10 13 } ;# "/" 4 moves / 0 s } 45 { set puzz { 5 3 0 8 } ;# "." 4 moves } 46 { set puzz { 5 3 0 2 8 14 } ;# ":" 5 moves / 1s } 47 { set puzz { 5 3 0 5 11 } ;# ":" 5 moves / ??? } 48 { set puzz { 5 3 0 5 8 11 } ;# ":" 5 moves / 1s } 49 { set puzz { 5 3 -1 2 8 14 } ;# ":" 5 moves / 1s } 50 { set puzz { 5 3 0 2 14 } ;# ":" 7 moves / 1s } 51 { set puzz { 5 3 0 2 5 8 11 14 7 9 } ;# "+" 6 moves / 1s } 52 { set puzz { 5 3 -1 4 7 10 6 9 12 } ;# "I" 4 moves / 1s } 53 { set puzz { 5 3 -1 2 4 5 6 10 11 12 14 } ;# ":-:" 6 moves / 1s } 54 { set puzz { 5 3 0 4 5 6 10 11 12 } ;# "=" 7 moves / 1s } 55 { set puzz { 5 3 -1 0 } ;# "@" 6 moves / 1s } 56 { set puzz { 5 4 0 20 } ;# "." 8 moves / 32 s } 57 { set puzz { 5 4 0 19 } ;# "." 8 moves / 30 s } 58 { set puzz { 5 4 0 1 2 3 4 14 15 } ;# "^-" 10 moves / 144-172 s } 59 { set puzz { 5 4 0 5 8 14 15 } ;# ":-" 8 moves / 65 s } 60 { set puzz { 5 4 0 6 7 10 11 14 15 } ;# "o" 12 moves / 266-348 s } 61 { set puzz { 5 4 -1 6 7 10 11 14 15 } ;# "O" 10 moves / 173 s } 62 { set puzz { 6 3 0 1 2 3 4 6 7 8 9 } ;# ".Q" 8 moves / 14s } 63 { set puzz { 6 3 0 5 10 12 16 18 } ;# "5." 9 moves / 24s } -64 { set puzz { 6 3 0 5 8 11 } ;# "D" 5 moves / 1s } 64 { set puzz { 6 3 -1 5 8 11 } ;# "D" 11 moves / 72s } 65 { set puzz { 6 3 -1 5 8 10 13 16 12 15 18} ;# "(-" 11 moves / 44s } 66 { set puzz { 6 3 0 1 3 5 7 8 9 11 14 17} ;# ">+-" 10 moves / 43s } 67 { set puzz { 6 3 -1 2 7 9 14 16 17 18 } ;# "X" 5 moves / 1s } 68 { set puzz { 6 3 0 1 3 7 9 13 15 17 } ;# "7." 6 moves / 4s } 69 { set puzz { 6 3 -1 2 4 6 } ;# "<" 12 moves / 66s-88s } 70 { set puzz { 6 3 -1 0 } ;# "@" 10 moves / 45s } 71 { set puzz { 5 5 0 1 2 4 5 6 10 16 20 21 22 24 25 } ;# "4C" 4 moves / 1s } default { set puzz { 5 5 0 1 } ;# 5x5 too big for solver set G(puz) 0 } } puts "Puzzle $::G(puz):" FillPuzzle $puzz if { $G(autoSolve) > 0 } {update; ::Solve::Hint 1} } proc FillPuzzle { p } { #: Fill puzzle-data into array PUZZ, for use in InitBoard global G PUZZ set r [lindex $p 0] set c [lindex $p 1] set inv [lindex $p 2] set G(rows) $r set G(cols) $c puts "$G(puz): $G(rows),$G(cols) $inv " ;## if {$inv == 0} { set X 1; set O 0 } else { set X 0; set O 1 } set i 0 for {set row 0} {$row < $G(rows)} {incr row} { for {set col 0} {$col < $G(cols)} {incr col} { incr i set PUZZ(board,$row,$col) $O if {[lsearch -start 2 $p $i] >= 2} { set PUZZ(board,$row,$col) $X } ## puts "$i - $row,$col: $PUZZ(board,$row,$col) " ;## } } NewBoard puzzle } proc DoCtrlFrame {} { button .restart -text "Restart Game" -command [list NewBoard last] -bd 4 .restart configure -font "[font actual [.restart cget -font]] -weight bold" option add *font [.restart cget -font] button .new -text "New Game" -bd 4 -command [list NewBoard rand] button .puzzle+ -text "Next Puzzle" -bd 4 -command { Puzzle 1 } button .puzzle- -text "Prev Puzzle" -bd 4 -command { Puzzle -1 } button .hint -text Hint -bd 4 -command ::Solve::Hint button .solution -text Solution -bd 4 -command {::Solve::Hint 1} scale .rows -from 1 -to 10 -label Rows -orient h -relief ridge \ -variable G(rows) -command [list NewBoard rand] scale .cols -from 1 -to 10 -label Columns -orient h -relief ridge \ -variable G(cols) -command [list NewBoard rand] label .moves -textvariable G(tmoves) -relief sunken -bg white button .help -text Help -command Help grid .new -in .ctrl -sticky ew -row 0 grid .restart -in .ctrl -sticky ew grid rowconfigure .ctrl 2 -minsize 20 grid .puzzle+ -in .ctrl -sticky ew -row 3 grid .puzzle- -in .ctrl -sticky ew grid rowconfigure .ctrl 5 -minsize 20 grid .solution -in .ctrl -sticky ew -row 6 grid .hint -in .ctrl -sticky ew grid rowconfigure .ctrl 10 -minsize 50 grid .rows -in .ctrl -sticky ew -row 12 grid .cols -in .ctrl -sticky ew grid rowconfigure .ctrl 15 -minsize 80 grid .moves -in .ctrl -sticky ew -row 15 grid rowconfigure .ctrl 50 -weight 1 grid .help -in .ctrl -sticky ew -row 100 } proc DrawBoard {{redraw 0}} { global S G NEIGHBORS .c delete msg hint if {$redraw} { ;# Redraw everything unset -nocomplain NEIGHBORS ;# Memoize array .c delete all set S(w) [winfo width .c] set S(h) [winfo height .c] set S(dx) [expr {double($S(w) - 10) / $G(cols)}] set S(dy) [expr {double($S(h) - 10) / $G(rows)}] if {$S(dx) < $S(dy)} {set S(dy) $S(dx)} else {set S(dx) $S(dy)} set S(x0) [expr {- $S(dx) * $G(cols) / 2}] set S(y0) [expr {- $S(dy) * $G(rows) / 2}] for {set row 0} {$row < $G(rows)} {incr row} { for {set col 0} {$col < $G(cols)} {incr col} { set xy [GetBox $row $col] .c create rect $xy -tag [list c c$row,$col] .c bind c$row,$col <Button-1> [list DoClick $row $col] } } set xy0 [GetBox 0 0] set xy1 [GetBox $G(rows) $G(cols)] set xy [concat [lrange $xy0 0 1] [lrange $xy1 0 1]] .c create rect $xy -width 3 } # Draw the light lattice set lite yellow if { [expr {( $G(cols) + $G(rows) ) & 1}] } { set lite cyan } for {set row 0} {$row < $G(rows)} {incr row} { for {set col 0} {$col < $G(cols)} {incr col} { set fill [.c cget -bg] if {$G(board,$row,$col) > 0} {set fill $lite} .c itemconfig c$row,$col -fill $fill } } if {$G(msg) ne ""} { .c create text 0 0 -tag msg -text $G(msg) -font {Times 36 bold} } } proc ShowHint {row col} { set xy [GetOval $row $col] .c create oval $xy -tag [list hint h$row,$col] -fill red .c bind h$row,$col <Button-1> [list DoClick $row $col] } proc GetBox {row col} { global S set x0 [expr {$S(x0) + $col * $S(dx)}] set y0 [expr {$S(y0) + $row * $S(dy)}] set x1 [expr {$x0 + $S(dx)}] set y1 [expr {$y0 + $S(dy)}] return [list $x0 $y0 $x1 $y1] } proc GetOval {row col} { global S set x0 [expr {$S(x0) + ($col + .33) * $S(dx)}] set y0 [expr {$S(y0) + ($row + .33) * $S(dy)}] set x1 [expr {$S(x0) + ($col + .66) * $S(dx)}] set y1 [expr {$S(y0) + ($row + .66) * $S(dy)}] return [list $x0 $y0 $x1 $y1] } proc Neighbors {row col} { global NEIGHBORS ;# Memoize if {[info exists NEIGHBORS($row,$col)]} { return $NEIGHBORS($row,$col) } set who {} foreach {dr dc} {-1 0 0 0 1 0 0 -1 0 1} { set r [expr {$row + $dr}] set c [expr {$col + $dc}] if {$r < 0 || $c < 0 || $r >= $::G(rows) || $c >= $::G(cols)} continue lappend who $r $c } set NEIGHBORS($row,$col) $who return $who } proc NewBoard {{how rand} args} { InitBoard $how DrawBoard 1 } proc InitBoard {how} { global G LAST PUZZ set G(state) play set G(msg) "" set G(moves) 0 set G(tmoves) "Moves: 0" set G(path) {} array unset G board,* puts "InitBoard $how" ;## if {$how eq "puzzle"} { array set G [array get PUZZ] array set LAST [array get G board,*] ;# Remember for restart return } if {$how eq "last"} { array set G [array get LAST] return } puts "InitBoard: $G(rows)x$G(cols) $how" ;## set G(puz) 0 while {1} { ;# Fill in the board for {set row 0} {$row < $G(rows)} {incr row} { for {set col 0} {$col < $G(cols)} {incr col} { set G(board,$row,$col) [expr {int(rand() * 2)}] ;# TODO ## puts "$row,$col: $G(board,$row,$col) " ;## } } if {! [IsWinner]} break ;# Must have 1 light on } array set LAST [array get G board,*] ;# Remember for restart } proc DoClick {row col} { if {$::G(state) ne "play"} return foreach {r c} [Neighbors $row $col] { set ::G(board,$r,$c) [expr {1 - $::G(board,$r,$c)}] } set ::G(tmoves) "Moves: [incr ::G(moves)]" lappend ::G(path) $row $col if {[IsWinner]} { set ::G(msg) "You Won!" set ::G(state) won } DrawBoard } proc IsWinner {} { foreach arr [array names ::G board,*] { if {$::G($arr) == 1} {return 0} } return 1 } proc Help {} { set msg "$::S(title)\nby Keith Vetter, September 2004\n\n" append msg "This is one-person puzzle in which you try to turn off\n" append msg "all the lights. The board consists of a lattice of lights\n" append msg "which can be turned on and off. Clicking on any light\n" append msg "toggles the on/off state of this and its four vertically\n" append msg "and horizontally adjacent neighbors.\n\n" append msg "Determining if a given random arrangement of on/off\n" append msg "lights can be all turned off is known as the\n" append msg "\"All-Ones Problem\"\n\n" append msg "Collection of puzzles, alternating colors\n" append msg "and solver-limiting by HaJo Gurt 2014-05-05\n\n" tk_messageBox -message $msg -title "$::S(title) Help" } puts "$::S(title) $::S(version)" InitBoard rand DoDisplay namespace eval ::Solve { variable b } proc ::Solve::Hint {{all 0}} { .c delete hint if {[::IsWinner]} return if { $::G(puz)==0 } { ;# not one of the stored puzzles -> check size set bs [expr {$::G(cols) * $::G(rows) }] if { $bs > $::G(maxSolve) } { set msg "Gameboard is too big for automatic solving." tk_messageBox -message $msg -title "$::S(title) - Solver disabled" return } } set path [::Solve::Solver] set ::T2 [clock seconds] set t [expr $::T2-$::T1] puts "done: $::T1 $::T2 => $t s [llength $path] moves\a" ;## if {$path == {}} { ;# No solution set msg "Can't be solved" .c create text 0 0 -tag msg -text $msg -font {Times 36 bold} return } if {! $all} { ;# Just one hint set path [lindex $path [expr {int(rand() * [llength $path])}]] } foreach step $path { scan $step "%d,%d" row col ShowHint $row $col } } proc ::Solve::Solve {} { global path set path [::Solve::Solver] puts "[llength $path] => $path" } proc ::Solve::Solver {} { variable b global G array unset b set moves {} for {set row 0} {$row < $G(rows)} {incr row} { for {set col 0} {$col < $G(cols)} {incr col} { lappend moves "$row,$col" set b($row,$col) $G(board,$row,$col) } } set max [llength $moves] set ::T1 [clock seconds] set td [clock format $::T1 -format "%Y-%m-%d %H:%M:%S" ] puts "Solve: $max $::T1 = $td" ;## array set save [array get b] for {set n 1} {$n <= $max} {incr n} { set all [::Solve::Combinations $moves $n] foreach path $all { array set b [array get save] foreach step $path { scan $step "%d,%d" row col ::Solve::DoMove $row $col } if {[::Solve::IsWinner]} { return $path } } } return {} } proc ::Solve::IsWinner {} { variable b foreach arr [array names b] { if {$b($arr) != 0} {return 0} } return 1 } proc ::Solve::DoMove {row col} { variable b set nb [Neighbors $row $col] foreach {row col} $nb { set b($row,$col) [expr {1 - $b($row,$col)}] } } proc ::Solve::Combinations {myList size {prefix {}}} { ;# End recursion when size is 0 or equals our list size if {$size == 0} {return [list $prefix]} if {$size == [llength $myList]} {return [list [concat $prefix $myList]]} set first [lindex $myList 0] set rest [lrange $myList 1 end] ;# Combine solutions w/ first element and solutions w/o first element set ans1 [::Solve::Combinations $rest [expr {$size-1}] \ [concat $prefix $first]] set ans2 [::Solve::Combinations $rest $size $prefix] return [concat $ans1 $ans2] } #.
GS (040911) A few years ago, there was an interesting discussion on sci.math about this puzzle [L1 ] and also a published article: Óscar Martín-Sánchez and Cristóbal Pareja-Flores, Two Reflected Analysis of Lights Out, Mathematics Magazine 74:4 (2001), 295-304. [L2 ]
Lars H: A rather elementary observation is that the problem can be stated as a linear equation system (one variable for each light) over GF(2), which means it can definitely be solved in polynomial time. The trivial complexity for an n-by-n grid is however O(n^6), so one might be interested in a faster solution.
HJG 2014-05-05: For big boards, like 5x5, 6x6, 7x7, or 7x10, Tcl even crashes:
5x5: alloc: could not allocate 800 new objects 6x6: alloc: could not allocate 800 new objects 7x7: Not enough memory to allocate list 7x10: unable to alloc 256 bytes ... This application has requested the Runtime to terminate it in an unusual way.
It looks like it consumed all the memory, and finally crashed (after about half an hour)...
Boards of size 5x4 can still be solved (it may need 3-6 minutes), but 7x3 needs about 10 minutes.
6x4, 5x5 and up are too big and lead to crashes.
Maybe disabling the Solve- and Hint-buttons on big boards would prevent such crashes (and frustration for players).