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 finish the program to play the game Lights Out.
Recall from last week that the object of Lights Out is to turn off all of the lights. The catch is that when you click on a light, that light toggles along with its neighbors (above, below, to the left and to the right).
When we finished last week, we had the game initially set up with a window showing the 5 X 5 grid, with some lights drawn in yellow using circles based on data we read from a data file. The state of each light is either 1 (on) or 0 (off) and this is stored in a two-dimensional array (i.e. an array of arrays). The board above is stored as follows in the variable named board:
In Python:
[[0,0,0,0,0],[0,0,1,0,0],[0,1,1,1,0],[0,0,1,0,0],[0,1,0,1,0]]
Conceptually:
column
0 1 2 3 4
row
0 0 0 0 0 0
1 0 0 1 0 0
2 0 1 1 1 1
3 0 0 1 0 0
4 0 1 0 1 0
Right-click HERE for a copy of the puzzle data
file for your computer.
Recall that 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. We have already set up the main function and the functions to perform the first three steps. Right-click HERE to download a fresh copy of what we completed last week.
The main function contains the following loop to control the game once it is set up:
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"
The variable game_over is a Boolean variable that can store the value True or False. These variables are similar to the variables you saw with the Satisfiability Problem. As long this variable is False, the game repeats the loop.
Each iteration waits for the user to click the mouse in the game window. The getMouse function returns the Point p where the user clicked. We can then use the getX and getY functions to get the x and y coordinates of this point. To determine the light row and light column that the user clicked, we can simply divide each y and x coordinate by 50 respectively, since each square region in the window is 50 X 50. For example, if the user clicks in the third row of lights (i.e. row #2 since we count rows from 0), the y coordinate must be between 100 and 149, so dividing by 50 yields 2 (remember that integer division is used here). Note here that the user doesn't have to click the circle itself, only the square that contains the circle.
Once we have the row and column that the user wants, we need to toggle that light and its neighbors. The update_board function will do this (see below). Once the board is updated, we display the lights again using the function we wrote last week.
Finally, we must check the board to see if all of the lights are off. We will do this with the check_for_winner function (also below). This function will return a new value for game_over. If the value is still False, the loop repeats and we wait for another mouse click. If the returned value is True, then the loop ends and the game ends.
When the function is executed, the main function passes the board to this function (so it has the state of all of the lights) and the row and column chosen by the user. These three data items are the parameters to this function. Python is set up so that the data is not globally accessible by all functions. Instead, if one function has some data (e.g. main) and another function needs it (e.g. update_board), then the former function must pass this data to the latter function using parameters.
To toggle the lights, we need to change the value of the board in the given row and given column from 1 to 0 or from 0 to 1, along with its neighbors (above, below, to the left and to the right). Keep in mind that the chosen light might not have all 4 neighbors. We can toggle the light that was chosen as follows:
if board[row][column] == 1: board[row][column] = 0 else: board[row][column] = 1
In the code above, the else does not need a check to see if board[row][column] is 0 since that is the only other option in this case.
We need to determine if this light has a light above it. The number of this row is row-1. There is a light above the chosen row as long as row-1 > 0, or if row > 0. (You can also say if row ≥ 1.) Here is the code we need:
if row > 0: if board[row-1][column] == 1: board[row-1][column] = 0 else: board[row-1][column] = 1
Be careful: when we change the array, make sure you change the light in row-1, not row. Also note the indentation above: the second if statement is only executed if the row chosen by the user is greater than 0. Otherwise, the entire indented block is skipped.
Here are the tests need to determine if there is a neighbor below, to the left and to the right of the chosen row and column:
There is a neighbor below if row < 4. There is a neighbor to the left if column > 0. There is a neighbor to the right if column < 4.
Here is the completed function:
def update_board(board, row, column):
# toggle chosen light in the given row and column
if board[row][column] == 1:
board[row][column] = 0
else:
board[row][column] = 1
if row > 0: # is there a light above the chosen light?
if board[row-1][column] == 1:
board[row-1][column] = 0
else:
board[row-1][column] = 1
if row < 4: # is there a light below the chosen light?
if board[row+1][column] == 1:
board[row+1][column] = 0
else:
board[row+1][column] = 1
if column > 0: # is there a light to the left of the chosen light?
if board[row][column-1] == 1:
board[row][column-1] = 0
else:
board[row][column-1] = 1
if column < 4: # is there a light to the right of the chosen light?
if board[row][column+1] == 1:
board[row][column+1] = 0
else:
board[row][column+1] = 1
You might think that we are changing a copy of the board in this function and not the original board in the main function, but we are changing the same array. When an array is passed from one function to another, a pointer to the array is sent rather than a copy of the array. So the update_board function is using the same array as the array in the main function, whether they have the same name or not. (This technique is called call-by-reference in programming terminology.) On the other hand, the row and column are copies of the original data values from main since these are only single data values. (This is called call-by-value.)
Run the program now and see if the lights toggle correctly.
Once the board is updated, how do we know if the player wins? If all of the lights are off, then every cell of the board array should be 0. If we add up all of the values in the array, the only way to get a final sum of 0 is if all of the lights are off. We just need a variable to keep track of the sum, initially equal to 0. Then we add each value of the sum, row by row, one column at a time in each row. This requires a set of nested loops as we discussed back in Unit 2. We then check the final sum to see if it is 0. If so, we return True (the game is over). Otherwise, we return False (the game is not over). This returned value is stored in the game_over variable in the main function to control the game loop.
Here is the code for our final function:
def check_for_winner(board):
sum = 0
for row in range(5):
for column in range(5):
sum = sum + board[row][column]
if sum == 0:
return True
else:
return False
Now play the game and see if you can win!
See if the game loads the data correctly. (Can you solve them?)