Chapter 10 - Lights Out

In the game Lights Out, you have a board of squares that are either light or dark. When you make a move on a certain space (indicated on the picture by the red dot) then that square, along with the squares above, below, left, and right of the space you moves will change color.

Light squares will become dark, and dark squares will become light:

The game starts out with a board that has several patches of light and dark squares. The player's goal is to make as few moves as possible to make the entire board dark. Here is an example of a starting board that is solved in four moves:

In our game, we will draw out the board with ASCII characters and use a field of 'O' characters to rperesent light squares, and a field of ' ' space characters to represent dark squares. The player will be able to select the size of the board and a difficulty level.

Sample Run

Welcome to Lights Out!
Do you want instructions? (yes/no)
no
What board size do you want to play? Enter size as WIDTHxHEIGHT.
For example, type 3x3 or 5x7. Min is 3x3, max is 10x10.
3x3
Enter the difficulty: (1-9)
3
You should be able to solve this in at least 9 moves.
    A   B   C
  +---+---+---+
  |OOO|   |   |
A |OOO|   |   |
  |OOO|   |   |
  +---+---+---+
  |OOO|   |OOO|
B |OOO|   |OOO|
  |OOO|   |OOO|
  +---+---+---+
  |   |   |   |
C |   |   |   |
  |   |   |   |
  +---+---+---+
Turns taken: 0 (Goal: 9)
Enter (A-C)(A-C), or quit, reset, or new:
ad
Enter (A-C)(A-C), or quit, reset, or new:
ab
    A   B   C
  +---+---+---+
  |   |   |   |
A |   |   |   |
  |   |   |   |
  +---+---+---+
  |   |OOO|OOO|
B |   |OOO|OOO|
  |   |OOO|OOO|
  +---+---+---+
  |OOO|   |   |
C |OOO|   |   |
  |OOO|   |   |
  +---+---+---+
Turns taken: 1 (Goal: 9)
Enter (A-C)(A-C), or quit, reset, or new:
bc
    A   B   C
  +---+---+---+
  |   |   |   |
A |   |   |   |
  |   |   |   |
  +---+---+---+
  |   |   |OOO|
B |   |   |OOO|
  |   |   |OOO|
  +---+---+---+
  |   |OOO|OOO|
C |   |OOO|OOO|
  |   |OOO|OOO|
  +---+---+---+
Turns taken: 2 (Goal: 9)
Enter (A-C)(A-C), or quit, reset, or new:
cc
    A   B   C
  +---+---+---+
  |   |   |   |
A |   |   |   |
  |   |   |   |
  +---+---+---+
  |   |   |   |
B |   |   |   |
  |   |   |   |
  +---+---+---+
  |   |   |   |
C |   |   |   |
  |   |   |   |
  +---+---+---+

**************************************************
Good job! You solved the puzzle in 3 moves.
This puzzle can be solved in at least 9 moves.
**************************************************

Do you want to reset this board, play a new game, or quit? (new/reset/quit)
quit
Thanks for playing!

Source Code

lightsout.py
  1. # Lights Out!
  2. import random # for the randint() function
  3. import math # for the sqrt() function
  4. import sys # for the exit() function
  5. LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  6. def enterBoardSize():
  7.     # Let player type in board size. Returns a two-item list of ints: [width, height]
  8.     print 'What board size do you want to play? Enter size as WIDTHxHEIGHT.'
  9.     print 'For example, type 3x4 or 5x3. Min is 3x3, max is 5x5.'
  10.     while True:
  11.         size = raw_input()
  12.         size = size.split('x')
  13.         if len(size) == 2 and isValidSize(size[0]) and isValidSize(size[1]):
  14.             return [int(size[0]), int(size[1])]
  15.         print 'Enter size as WIDTHxHEIGHT. Min size is 3, max size is 5.'
  16. def isValidSize(n):
  17.     # Returns True if n is a number and between 3 and 5. Otherwise returns False.
  18.     return n.isdigit() and int(n) >= 3 and int(n) <= 5
  19. def enterDifficulty():
  20.     # Let the player type in the difficulty level.
  21.     while True:
  22.         print 'Enter the difficulty: (1-9)'
  23.         diff = raw_input()
  24.         if diff.isdigit() and int(diff) >= 1 and int(diff) <= 9:
  25.             return int(diff)
  26. def getWidthHeight(board):
  27.     # Returns a list [width, height] for the given board data structure.
  28.     return [len(board), len(board[0])]
  29. def drawBoard(board):
  30.     # Print the board on the screen.
  31.     width, height = getWidthHeight(board)
  32.     print '    ' + '   '.join(LETTERS[:width])
  33.     print '  +' + ('---+' * (width))
  34.     for y in range(height):
  35.         print '  |' + '|'.join(getRow(board, y)) + '|'
  36.         print LETTERS[y] + ' |' + '|'.join(getRow(board, y)) + '|'
  37.         print '  |' + '|'.join(getRow(board, y)) + '|'
  38.         print '  +' + ('---+' * (width))
  39. def getBoardSymbol(boardValue):
  40.     # Returns string 'O' if boardValue is True, otherwise returns string '.'
  41.     if boardValue:
  42.         return 'O'
  43.     else:
  44.         return '.'
  45. def getRow(board, row):
  46.     # Returns a list of the board spaces for the row number given.
  47.     width, height = getWidthHeight(board) # height isn't used in this function
  48.     boardRow = []
  49.     row = int(row)
  50.     for i in range(width):
  51.         boardRow.append(getBoardSymbol(board[i][row]) * 3) # * 3 because each space is 3x3 characters
  52.     return boardRow
  53. def getNewBoard(width, height):
  54.     # Creates a brand new, blank board data structure.
  55.     board = []
  56.     for i in range(width):
  57.         board.append([False] * height)
  58.     return board
  59. def getBoardCopy(board):
  60.     # Return a copy of the board data structure.
  61.     width, height = getWidthHeight(board)
  62.     dupeBoard = getNewBoard(width, height)
  63.     for x in range(width):
  64.         for y in range(height):
  65.             dupeBoard[x][y] = board[x][y]
  66.     return dupeBoard
  67. def randomMoves(board, numMoves):
  68.     # Make a number of random moves on the board.
  69.     width, height = getWidthHeight(board)
  70.     for i in range(numMoves):
  71.         makeMove(board, random.randint(0, width-1), random.randint(0, height-1))
  72. def letterToIndex(letter):
  73.     # Return an integer for each letter, 0 for 'A', 1 for 'B', etc.
  74.     return LETTERS.find(letter)
  75. def isOnBoard(board, x, y):
  76.     # Return True if XY coordinates are on the board.
  77.     width, height = getWidthHeight(board)
  78.     return x >= 0 and x < width and y >= 0 and y < height
  79. def makeMove(board, x, y):
  80.     # Make a move on the given board data structure.
  81.     if isOnBoard(board, x, y): # flip space
  82.         board[x][y] = not board[x][y]
  83.     if isOnBoard(board, x, y-1): # flip space above
  84.         board[x][y-1] = not board[x][y-1]
  85.     if isOnBoard(board, x, y+1): # flip space below
  86.         board[x][y+1] = not board[x][y+1]
  87.     if isOnBoard(board, x-1, y): # flip space to the left
  88.         board[x-1][y] = not board[x-1][y]
  89.     if isOnBoard(board, x+1, y): # flip space to the right
  90.         board[x+1][y] = not board[x+1][y]
  91. def enterMove(board):
  92.     # Let player enter their next move.
  93.     width, height = getWidthHeight(board)
  94.     while True:
  95.         print 'Enter (A-%s)(A-%s), or quit, reset, or new:' % (LETTERS[width-1], LETTERS[height-1])
  96.         move = raw_input().upper()
  97.         if move == 'QUIT' or move == 'RESET' or move == 'NEW':
  98.             return move
  99.         elif len(move) == 2 and move.isalpha() and isOnBoard(board, letterToIndex(move[0]), letterToIndex(move[1])):
  100.             return [letterToIndex(move[0]), letterToIndex(move[1])]
  101. def playAgain():
  102.     # Ask player if they want to play again.
  103.     print 'Do you want to reset this board, play a new game, or quit? (new/reset/quit)'
  104.     while True:
  105.         action = raw_input().lower()
  106.         if action.startswith('r'):
  107.             return 'reset'
  108.         elif action.startswith('n'):
  109.             return 'new'
  110.         elif action.startswith('q'):
  111.             return 'quit'
  112.         print 'Please type in new, reset, or quit.'
  113.                 
  114. def isBoardOff(board):
  115.     # Return True if the entire board is "off"
  116.     width, height = getWidthHeight(board)
  117.     for x in range(width):
  118.         for y in range(height):
  119.             if board[x][y]:
  120.                 return False
  121.     return True
  122. def showInstructions():
  123.     # Print out instructions.
  124.     print '''Instructions:
  125. The Lights Out board is a made up of on (O) and off (.) lights.
  126. Pushing down a light will reverse its state, and also the states of
  127. the lights above, below, and to the left and right of it. (On lights
  128. will turn off, off lights will turn on.)
  129. The goal of the game is to turn off all the lights on the board in
  130. as few moves as possible.
  131. To enter a move, type the letter of the column followed by the letter
  132. of the row you wish to push.
  133. You can also type quit to exit the game, or type reset to start the
  134. current puzzle over.
  135. You can also type new to start a new game.'''
  136.     print
  137. print 'Welcome to Lights Out!'
  138. print 'Do you want instructions? (yes/no)'
  139. if raw_input().lower().startswith('y'):
  140.     showInstructions()
  141. while True:
  142.     width, height = enterBoardSize()
  143.     diff = enterDifficulty()
  144.     theBoard = getNewBoard(width, height)
  145.     # Make several random moves on the board depending on board size and difficulty.
  146.     # round() returns a float, so call int() to cut off the trailing .0
  147.     fewestMoves = diff * int(round(math.sqrt(width * height)))
  148.     randomMoves(theBoard, fewestMoves)
  149.     print 'You should be able to solve this in at least %s moves.' % (fewestMoves)
  150.     originalBoard = getBoardCopy(theBoard) # for when the player wants to reset the board
  151.     movesTaken = 0
  152.     while True:
  153.         drawBoard(theBoard)
  154.         if isBoardOff(theBoard):
  155.             # Player has won the game
  156.             print
  157.             print '*' * 50
  158.             print 'Good job! You solved the puzzle in %s moves.' % (movesTaken)
  159.             print 'This puzzle can be solved in at least %s moves.' % (fewestMoves)
  160.             print '*' * 50
  161.             print
  162.             action = playAgain()
  163.             if action == 'reset':
  164.                 movesTaken = 0
  165.                 theBoard = getBoardCopy(originalBoard)
  166.                 continue
  167.             elif action == 'new':
  168.                 break
  169.             else: # default of quit
  170.                 print 'Thanks for playing!'
  171.                 sys.exit()
  172.         print 'Turns taken: %s (Goal: %s)' % (movesTaken, fewestMoves)
  173.         move = enterMove(theBoard)
  174.         movesTaken += 1
  175.         if move == 'RESET':
  176.             movesTaken = 0
  177.             theBoard = getBoardCopy(originalBoard)
  178.         elif move == 'QUIT':
  179.             print 'Thanks for playing!'
  180.             sys.exit()
  181.         elif move == 'NEW':
  182.             break
  183.         else:
  184.             makeMove(theBoard, move[0], move[1])

Code Explanation

  1. # Lights Out!
  2. import random # for the randint() function
  3. import math # for the sqrt() function
  4. import sys # for the exit() function
  5. LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

Here we import the four modules that our Lights out game will need. The math is a new module which has the sqrt() function (called the "square root" function). We also define a variable named LETTERS, which is in all uppercase letters to remind us that this is a constant variable and we should not change it's value. The reason we define this variable is so that we can type LETTERS instead of 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' each time we use it in our program.


  1. def enterBoardSize():
  2.     # Let player type in board size. Returns a two-item list of ints: [width, height]
  3.     print 'What board size do you want to play? Enter size as WIDTHxHEIGHT.'
  4.     print 'For example, type 3x4 or 5x3. Min is 3x3, max is 5x5.'

The enterBoardSize() lets the player type in what size they want the game board to be.


  1.     while True:
  2.         size = raw_input()
  3.         size = size.split('x')
  4.         if len(size) == 2 and isValidSize(size[0]) and isValidSize(size[1]):
  5.             return [int(size[0]), int(size[1])]

The while loop will only be broken out of by the return statement, otherwise we will keep asking the player for a size. The variable size has the split() method called on it. If size has one (and only one) 'x' in it, then the method will return a two item list. Line 14 assigns this list as the new value of size.

If size is not a two item list, then len(size) == 2 will be True and due to the short-circuiting of and operators, the isValidSize(size[0]) and isValidSize(size[1]) part of the expression will not be evaluated. This is good, because if the length of size is only 1, then size[1] part will cause an error.

The isValidSize() function is one we will create, and it returns True if the integer form of the argument you pass is between 3 and 10.

Our enterBoardSize() function returns a two item list with the two numbers the player typed in (if those numbers are valid).


  1.         print 'Enter size as WIDTHxHEIGHT. Min size is 3, max size is 5.'
  1. def isValidSize(n):
  2.     # Returns True if n is a number and between 3 and 5. Otherwise returns False.
  3.     return n.isdigit() and int(n) >= 3 and int(n) <= 5
  1. def enterDifficulty():
  2.     while True:
  3.         print 'Enter the difficulty: (1-9)'
  4.         diff = raw_input()
  5.         if diff.isdigit() and int(diff) >= 1 and int(diff) <= 9:
  6.             return int(diff)
  1. def getWidthHeight(board):
  2.     return [len(board), len(board[0])]
  1. def drawBoard(board):
  2.     width, height = getWidthHeight(board)
  3.     print '    ' + '   '.join(LETTERS[:width])
  4.     print '  +' + ('---+' * (width))
  1.     for y in range(height):
  2.         print '  |' + '|'.join(getRow(board, y)) + '|'
  3.         print LETTERS[y] + ' |' + '|'.join(getRow(board, y)) + '|'
  4.         print '  |' + '|'.join(getRow(board, y)) + '|'
  5.         print '  +' + ('---+' * (width))
  1. def getBoardSymbol(boardValue):
  2.     # Returns 'O' if boardValue is True, otherwise '.'
  3.     if boardValue:
  4.         return 'O'
  5.     else:
  6.         return '.'
  1. def getRow(board, row):
  2.     # Returns a list of the board spaces for the row number given.
  3.     width, height = getWidthHeight(board) # height isn't used in this function
  4.     boardRow = []
  5.     row = int(row)
  6.     for i in range(width):
  7.         boardRow.append(getBoardSymbol(board[i][row]) * 3) # * 3 because each space is 3x3 characters
  8.     return boardRow
  1. def getNewBoard(width, height):
  2.     # Creates a brand new, blank board data structure.
  3.     board = []
  4.     for i in range(width):
  5.         board.append([False] * height)
  6.     return board
  1. def getBoardCopy(board):
  2.     width, height = getWidthHeight(board)
  3.     dupeBoard = getNewBoard(width, height)
  1.     for x in range(width):
  2.         for y in range(height):
  3.             dupeBoard[x][y] = board[x][y]
  4.     return dupeBoard
  1. def randomMoves(board, numMoves):
  2.     width, height = getWidthHeight(board)
  3.     for i in range(numMoves):
  4.         makeMove(board, random.randint(0, width-1), random.randint(0, height-1))
  1. def letterToIndex(letter):
  2.     return LETTERS.find(letter)
  1. def isOnBoard(board, x, y):
  2.     width, height = getWidthHeight(board)
  3.     return x >= 0 and x < width and y >= 0 and y < height
  1. def makeMove(board, x, y):
  2.     if isOnBoard(board, x, y): # flip space
  3.         board[x][y] = not board[x][y]
  4.     if isOnBoard(board, x, y-1): # flip space above
  5.         board[x][y-1] = not board[x][y-1]
  6.     if isOnBoard(board, x, y+1): # flip space below
  7.         board[x][y+1] = not board[x][y+1]
  8.     if isOnBoard(board, x-1, y): # flip space to the left
  9.         board[x-1][y] = not board[x-1][y]
  10.     if isOnBoard(board, x+1, y): # flip space to the right
  11.         board[x+1][y] = not board[x+1][y]
  1. def enterMove(board):
  2.     width, height = getWidthHeight(board)
  3.     while True:
  4.         print 'Enter (A-%s)(A-%s), or quit, reset, or new:' % (LETTERS[width-1], LETTERS[height-1])
  5.         move = raw_input().upper()
  1.         if move == 'QUIT' or move == 'RESET' or move == 'NEW':
  2.             return move
  1.         elif len(move) == 2 and move.isalpha() and isOnBoard(board, letterToIndex(move[0]), letterToIndex(move[1])):
  2.             return [letterToIndex(move[0]), letterToIndex(move[1])]
  1. def playAgain():
  2.     print 'Do you want to reset this board, play a new game, or quit? (new/reset/quit)'
  3.     while True:
  4.         action = raw_input().lower()
  5.         if action.startswith('r'):
  6.             return 'reset'
  7.         elif action.startswith('n'):
  8.             return 'new'
  9.         elif action.startswith('q'):
  10.             return 'quit'
  11.         print 'Please type in new, reset, or quit.'
  12.                 
  1. def isBoardOff(board):
  2.     width, height = getWidthHeight(board)
  3.     for x in range(width):
  4.         for y in range(height):
  5.             if board[x][y]:
  6.                 return False
  7.     return True
  1. def showInstructions():
  2.     print '''Instructions:
  3. The Lights Out board is a made up of on (O) and off (.) lights.
  4. Pushing down a light will reverse its state, and also the states of
  5. the lights above, below, and to the left and right of it. (On lights
  6. will turn off, off lights will turn on.)
  7. The goal of the game is to turn off all the lights on the board in
  8. as few moves as possible.
  9. To enter a move, type the letter of the column followed by the letter
  10. of the row you wish to push.
  11. You can also type quit to exit the game, or type reset to start the
  12. current puzzle over.
  13. You can also type new to start a new game.'''
  14.     print
  1. print 'Welcome to Lights Out!'
  2. print 'Do you want instructions? (yes/no)'
  3. if raw_input().lower().startswith('y'):
  4.     showInstructions()
  1. while True:
  2.     width, height = enterBoardSize()
  3.     diff = enterDifficulty()
  1.     theBoard = getNewBoard(width, height)
  2.     # Make several random moves on the board depending on board size and difficulty.
  3.     # round() returns a float, so call int() to cut off the trailing .0
  4.     fewestMoves = diff * int(round(math.sqrt(width * height)))
  5.     randomMoves(theBoard, fewestMoves)
  6.     print 'You should be able to solve this in at least %s moves.' % (fewestMoves)
  7.     originalBoard = getBoardCopy(theBoard) # for when the player wants to reset the board
  8.     movesTaken = 0
  1.     while True:
  2.         drawBoard(theBoard)
  1.         if isBoardOff(theBoard):
  2.             # Player has won the game
  3.             print
  4.             print '*' * 50
  5.             print 'Good job! You solved the puzzle in %s moves.' % (movesTaken)
  6.             print 'This puzzle can be solved in at least %s moves.' % (fewestMoves)
  7.             print '*' * 50
  8.             print
  1.             action = playAgain()
  2.             if action == 'reset':
  3.                 movesTaken = 0
  4.                 theBoard = getBoardCopy(originalBoard)
  5.                 continue
  1.             elif action == 'new':
  2.                 break
  1.             else: # default of quit
  2.                 print 'Thanks for playing!'
  3.                 sys.exit()
  1.         print 'Turns taken: %s (Goal: %s)' % (movesTaken, fewestMoves)
  1.         move = enterMove(theBoard)
  2.         movesTaken += 1
  3.         if move == 'RESET':
  4.             movesTaken = 0
  5.             theBoard = getBoardCopy(originalBoard)
  6.         elif move == 'QUIT':
  7.             print 'Thanks for playing!'
  8.             sys.exit()
  9.         elif move == 'NEW':
  10.             break
  1.         else:
  2.             makeMove(theBoard, move[0], move[1])

Things Covered In This Chapter: