15-105 SPRING 2009 [CORTINA]

LAB 8 - Lights Out (Part 1)

If you don't have a copy of the graphics module we used last week, download a copy of this module by clicking HERE. Once this file is downloaded to your desktop, move it to your Python working area (or work on the desktop). This Python module must be in the same folder as any program code that uses it.

In this two part lab, we will create a program to play the game Lights Out.

Reading from a File

Quite often, data that we need for a program will be stored in a data file so we don't have to enter the data one by one using the keyboard. Recall the program we used to find the maximum of a set of 5 numbers:

def main():

        A = [4, 7, 3, 9, 8]

	i = 0
        max = A[0]
	while i < len(A)-1:
		i = i + 1
                if A[i] > max:
                        max = A[i]

        print "The maximum positive integer is", max

main()

Let's assume that there is a text file that contains the 5 data values shown above, one per line. We can modify the program to initialize the array with 5 data values from a file as follows:

def main():

        A = []      # start with an empty array
        filename = raw_input("Input filename containing data: ")
        infile = open(filename, "r")
        for j in range(5):
             number = eval(infile.readline())
             A.append(number)

	i = 0
        max = A[0]
	while i < len(A)-1:
		i = i + 1
                if A[i] > max:
                        max = A[i]

        print "The maximum positive integer is", max

main()

Assume there is a text file (created using pico) named nums.txt in the same folder as our program, and it contains the following data:

4
7
3
9
8

The program starts with an empty array A. It then asks the user for the name of the data file using the raw_input function. For example, the user might enter nums.txt. We then open the file for reading using the open function. The "r" means we want to read from the file specified by the filename. The function returns a link to the file which we store in the variable infile. Then whenever we want to go to the file for data, we use the variable infile.

The program now loops 5 times and reads each line of the file in one at a time, converting the string read in to a value (number) using eval(infile.readline()), and then appending this value to the array. The expression infile.readline() reads the next line from the text file, and the function eval converts it to a number since it is read in as a string.

The Lights Out Game

The Lights Out game is a popular game that consists of a 5 X 5 grid of lights. When the game starts, some of the lights are on and some are off, as shown below.

The object of the game is to turn all of the lights off. When you click on a light, it toggles (from off to on, or from on to off) and so does its four neighbors (above, below, to the left and to the right). Not all lights have 4 neighbors, however. For example, the corner lights only have 2 neighbors.

The initial state of the lights will be given by a data file, where 0 represents off and 1 represents on. The file will have 25 numbers, one for each light, one per line, in order row by row, left to right. Click HERE to see how the game above would be stored in the file. (Right click it to save it to your desktop.)

Basic game algorithm

The game follows the following algorithm:

1. Draw the lines for the board. 
2. Initialize the game board lights. 
3. Draw all of the lights on the board. 
4. Set GameOver to False. 
5. While GameOver is False do the following: 
   a. Get the row and column of the light button that is clicked. 
   b. Toggle the chosen light and its neighbors. 
   c. Draw all of the lights on the board. 
   d. If all of the lights on the board are off, set GameOver to True. 
6. Output "YOU WIN!" 

Each of the major steps of the algorithm will be represented by Python functions. Here is the main function and the other functions, empty for now:

# lightsout.py
# Program that simulates a lights-out game

from graphics import *

def main():
        filename = raw_input("Input name of puzzle file: ")
        window = GraphWin("Lights Out", 250, 250)

        board = initialize_board(filename)
        draw_lines(window)
        display_lights(window,board)
        game_over = False
        while game_over == False:
                p = window.getMouse()
                print p.getX()," ",p.getY()
                col = p.getX()/50     # compute column where player clicked
                row = p.getY()/50     # compute row where player clicked
                update_board(board, row, col)
                display_lights(window,board)
                game_over = check_for_winner(board)
        print "GAME OVER"
        raw_input("Press <ENTER> to quit.")
        window.close()

You can download a copy of this to get started by right-clicking HERE to save the file.

The main function starts by asking the user for the name of the file containing the data for the lights. It opens a 250 X 250 window for the game and initializes the board array with the data in the file using another function named initialize_board. It then draws the horizontal and vertical lines in the window to divide it into 25 equal squares and then draws the circular lights, one per square, using the draw_lines and display_lights functions. We will write these functions today.

Initializing the board

The variable board represents a two-dimensional array (an array of arrays). To create it, we have to create a 5 X 5 array, we have to create an empty array and then append 5 empty arrays to it:

[ [], [], [], [], [] ]

The following code will read in the 25 light values from our data file and append them to this array:

def initialize_board(filename):
        infile = open(filename, "r")
        board = []
        for i in range(5):
                board.append([])
        for row in range(5):
                for col in range(5):
                        columnvalue = eval(infile.readline())
                        board[row].append(columnvalue)
        return board

The first four lines create the empty array of arrays. We then have a nested loop that reads in the 25 values for the array of arrays.

Drawing the lines

Drawing the 8 lines needed to divide up the window is straight-forward:

def draw_lines(window):
        line1 = Line(Point(0,50),Point(250,50))
        line1.draw(window)  
        line2 = Line(Point(0,100),Point(250,100))
        line2.draw(window)
        line3 = Line(Point(0,150),Point(250,150))
        line3.draw(window)
        line4 = Line(Point(0,200),Point(250,200))
        line4.draw(window)
        line5 = Line(Point(50,0),Point(50,250))
        line5.draw(window)
        line6 = Line(Point(100,0),Point(100,250))
        line6.draw(window)
        line7 = Line(Point(150,0),Point(150,250))
        line7.draw(window)
        line8 = Line(Point(200,0),Point(200,250))
        line8.draw(window)

Drawing the lights

To draw the lights, we need to draw 25 circles, filled in with white (off) or yellow (on). Here are the rows and columns of the game, along with the location of the centers of the circles in the window:

row   column   center for circle in window
0     0        25, 25
0     1        75, 25
0     2        125,25
0     3        175,25
0     4        225,25
1     0        25, 75
1     1        75, 75
1     2        125,75
1     3        175,75
1     4        225,75
2     0        25, 125
2     1        75, 125
2     2        125,125
2     3        175,125
2     4        225,125
etc.

Do you see the pattern here? For a given row and column, the circle has a center at (column*50+25, row*50+25). All circles have a radius of 25. Here is a function that displays the lights of the board in the window:

def display_lights(window,board):
        for row in range(5):
                for column in range(5):
                        center = Point(column*50+25,row*50+25)
                        circ = Circle(center,25)
                        if (board[row][column] == 1):
                                circ.setFill("yellow")
                        else:
                                circ.setFill("white")
                        circ.draw(window)

Run this program and see if you see the game board set up as expected.

PRACTICE EXERCISES

  1. Create a new data file that contains a different game configuration, and run the program with this data file to see if it displays correctly.

  2. Look at the draw_lines function. Instead of drawing 4 vertical lines individually and 4 horizontal lines individually, rewrite this function so it uses 2 loops, one will draw the horizontal lines, and then the 2nd loop will draw the vertical lines. Do NOT nest these loops one inside the other. Look at the original code and find the pattern.

  3. [HARDER] Can you determine an algorithm to solve the puzzle shown in the picture above?