Lights Out
I discovered a variant of the lights-out puzzle which I find strangely
addicting. The board is large (12 by 12). Pressing a light toggles it
as well as its neighbors. On the N
‘th level, the computer “presses”
4N
lights at random. Your task is to undo the presses. It is an
exercise in pattern matching. The task gets more difficult at later
levels as the overlapping patterns begin to look random, but at the
same time you have more clicks available because the computer may
choose a location more than once (and pressing a light an even number
of times is the same as not pressing it at all). If you learned to
beat the game, you would be able to solve the 12x12 lights-out puzzle.
Notice that the order of clicks is unimportant (the operation is
commutative), and that clicking a location K
times is the same as
clicking it K%2
times. Thus a solution can be described by a bitmap
over the board.
And the source (uses Pygame):
import pygame
import random
from pygame.locals import *
'''
A strangely addicting game, based on the Lights Out puzzle.
Justin Pombrio, 2009
'''
START_LEVEL = 1
DIFFICULTY_INCREASE = 4
BACKGROUND = pygame . Color ( 130 , 130 , 130 )
CELL_SIZE = 64
GRID_SIZE = ( 12 , 12 )
TOP_HEIGHT = 60
LINE_WIDTH = 2
LIGHT_BORDER = 4
INNER_LIGHT_BORDER = 16
TOP_COLOR = pygame . Color ( 240 , 240 , 240 )
LINE_COLOR = pygame . Color ( 80 , 60 , 40 )
CELL_COLOR = pygame . Color ( 160 , 130 , 80 )
LIGHT_COLOR = pygame . Color ( 150 , 30 , 30 )
INNER_LIGHT_COLOR = pygame . Color ( 160 , 35 , 35 )
TEXT_COLOR = pygame . Color ( 0 , 0 , 0 )
TEXT_SIZE = 26
TEXT_POSITIONS = [( 20 , 20 ), ( 200 , 20 ), ( 400 , 20 )]
class Grid :
def __init__ ( self , size ):
self . width = size [ 0 ]
self . height = size [ 1 ]
self . lights = [[ False for y in range ( self . height )]
for x in range ( self . width )]
def clear ( self ):
for x in range ( self . width ):
for y in range ( self . height ):
self . lights [ x ][ y ] = False
def valid ( self , light ):
x , y = light
return x >= 0 and y >= 0 and x < self . width and y < self . height
def toggle ( self , light ):
if self . valid ( light ):
x , y = light
self . lights [ x ][ y ] = not self . lights [ x ][ y ]
def press_random ( self ):
"""Press a random cell."""
x = random . randrange ( 0 , GRID_SIZE [ 0 ])
y = random . randrange ( 0 , GRID_SIZE [ 1 ])
self . press (( x , y ))
def press ( self , light ):
x , y = light
affected_lights = [( x , y ), ( x - 1 , y ), ( x + 1 , y ), ( x , y - 1 ), ( x , y + 1 )]
for light in affected_lights :
self . toggle ( light )
def empty ( self ):
for x in range ( GRID_SIZE [ 0 ]):
for y in range ( GRID_SIZE [ 1 ]):
if self . lights [ x ][ y ]:
return False
return True
def full ( self ):
for x in range ( GRID_SIZE [ 0 ]):
for y in range ( GRID_SIZE [ 1 ]):
if not self . lights [ x ][ y ]:
return False
return True
class Game :
def __init__ ( self ):
pygame . init ()
self . grid = Grid ( GRID_SIZE )
self . font = pygame . font . Font ( pygame . font . get_default_font (), TEXT_SIZE )
screen_size = ( CELL_SIZE * GRID_SIZE [ 0 ],
CELL_SIZE * GRID_SIZE [ 1 ] + TOP_HEIGHT )
self . screen = pygame . display . set_mode ( screen_size )
self . max_level = 0
self . load_level ( START_LEVEL )
while True :
for event in pygame . event . get ():
if event . type == pygame . QUIT :
return
elif event . type == pygame . KEYDOWN :
if event . key == pygame . K_ESCAPE :
return
elif event . type == pygame . MOUSEBUTTONDOWN :
if event . button == 1 : # Left click
self . press ( event . pos )
def to_cell ( self , pos ):
return ( int ( pos [ 0 ] / CELL_SIZE ), int (( pos [ 1 ] - TOP_HEIGHT ) / CELL_SIZE ))
def to_screen ( self , pos ):
return ( pos [ 0 ] * CELL_SIZE , pos [ 1 ] * CELL_SIZE + TOP_HEIGHT )
def draw ( self ):
self . screen . fill ( LINE_COLOR )
top = pygame . Rect ( 0 , 0 , self . screen . get_width (), TOP_HEIGHT )
pygame . draw . rect ( self . screen , TOP_COLOR , top )
for x in range ( GRID_SIZE [ 0 ]):
for y in range ( GRID_SIZE [ 1 ]):
self . draw_cell ( x , y , self . grid . lights [ x ][ y ])
self . draw_text ( "Level: " + str ( self . level ), TEXT_POSITIONS [ 0 ])
self . draw_text ( "Clicks left: " + str ( self . clicks ), TEXT_POSITIONS [ 1 ])
self . draw_text ( "Best level: " + str ( self . max_level ), TEXT_POSITIONS [ 2 ])
pygame . display . flip ()
def _get_rect ( self , pos , border ):
return pygame . Rect ( pos [ 0 ] + border , pos [ 1 ] + border ,
CELL_SIZE - 2 * border , CELL_SIZE - 2 * border )
def draw_cell ( self , x , y , is_on ):
pos = self . to_screen (( x , y ))
rect = self . _get_rect ( pos , LINE_WIDTH )
light_rect = self . _get_rect ( pos , LIGHT_BORDER )
inner_light_rect = self . _get_rect ( pos , INNER_LIGHT_BORDER )
pygame . draw . rect ( self . screen , CELL_COLOR , rect )
if is_on :
pygame . draw . ellipse ( self . screen , LIGHT_COLOR , light_rect )
def draw_text ( self , text , pos ):
text_image = self . font . render ( text , True , TEXT_COLOR )
self . screen . blit ( text_image , pos )
def press ( self , pos ):
cell = self . to_cell ( pos )
if self . grid . valid ( cell ):
self . clicks = self . clicks - 1
self . grid . press ( self . to_cell ( pos ))
self . draw ()
if self . grid . empty () or self . grid . full ():
self . load_level ( self . level + 1 )
elif self . clicks == 0 :
self . load_level ( self . level - 1 )
def load_level ( self , level ):
self . clicks = 0
self . level = level
self . max_level = max ( level , self . max_level )
difficulty = DIFFICULTY_INCREASE * level
self . clicks = difficulty
self . grid . clear ()
for i in range ( difficulty ):
self . grid . press_random ()
self . draw ()
if __name__ == '__main__' :
game = Game ()
pygame . quit ()