Tic-Tac-Toe - Software Projects - Raspberry Pi Projects (2014)

Raspberry Pi Projects (2014)

Part II. Software Projects

Chapter 3. Tic-Tac-Toe

by Mike Cook

In This Chapter

• Learn more about lists

• Make a list of lists

• Understand what a test harness is

• Get robust user input

• Implement increasingly sophisticated levels of artificial intelligence

Tic-tac-toe is better known as noughts and crosses in Europe. So when the 1956 American TV show Tic Tac Dough, which had a theme tune that followed the rhythm of those words, was transferred to the U.K., they changed the name to Criss Cross Quiz to keep the same rhythm and because no one in the U.K. knew the game as tic-tac-toe. On U.S. TV the show was revived in 1978 and in 1980 became one of the first TV shows to have its graphics generated by a computer. Each square was controlled by one Apple II computer, with the whole array being controlled by an Altair 8800 computer system. So it is perhaps fitting that tic-tac-toe should be the subject of this book’s first software project. In fact, I made a noughts and crosses automatic playing machine simply out of multipole switches and flashlight bulbs way back in the mid-60s for a school open day.

I am sure that you know how to play tic-tac-toe, but just in case this book reaches places where it is not a familiar game, I’ll go over how it is played. The game is played on a 3 X 3 grid formed by drawing two horizontal lines and two vertical lines. Two players in turn mark an empty square with a cross or a zero. The first player to have three of their marks in a line is the winner. The line can be horizontal, vertical or diagonal.

Tic-tac-toe is an ideal game for learning how to program because it uses so many concepts that are useful in computer programming, as well as having the virtue that it can be built up gradually in a step-by-step manner. This is the way that all projects should be developed, one small step at a time. Nobody writes a large program and then runs it for the first time, except perhaps beginners. This is because by writing small and testing, you drastically reduce where any errors can be. If your first bit has no errors, when you write the next bit, and suddenly there is an error, then it is almost certain that your error lies in the code you have just written.

Errors

When you write code you can make two sorts of errors: The first sort, known as a syntax error, is when the computer can’t make sense of the instructions you have given it. Examples of these types of errors are spelling variable names incorrectly, not adhering to the same uppercase and lowercase mix in variables and not getting the format of a command correct, such as forgetting to add a colon. The second sort, known as a logical error, is much harder to spot. These errors are when the computer does exactly what you told it to do, but what you told it to do was not exactly what you wanted it to do. Logical errors can be the hardest to find. This often fools beginners because, when the computer is finished complaining about errors, they expect a program to run like they thought they wrote it. In this case logical errors are caused by the programmer’s thinking more like a human and not enough like a computer. The more experienced you get, the better you will become at avoiding these errors, but it is human nature that you will never be entirely free of them; you will just get better at spotting them and tracking them down.

It is important to realise that there is no one correct way to write a program, despite what some programmers might want to think. The forums are full of raging arguments as to the best style and technique to use, but do not be intimidated by this. Good code is code that does what you want it to. Better code is code that does this in a concise, easy-to-understand way. After that you get some code that is more memory efficient, some that is more efficient in the number of code lines it uses and others that execute faster. Do not get bogged down by this; code that takes a 0.25 second to run between user inputs is totally indistinguishable from code that takes 0.25uS to run. However, context is everything, so code that takes 25 seconds to run will win a lot less friends than 0.25 second code.

Making a Start

In the preceding chapter, “Introductory Software Project: The Insult Generator”, you can see how you could make a list of words, such as nouns and verbs, for the insult generator. Lists are very useful, and you will use them many times in programming. In this chapter you will use a list to represent the tic-tac-toe playing board. Each position in the list represents a square, and the contents of each position represents the contents of the square. This is shown in Figure 3.1.

Figure 3-1: The relationship of the board to the list.

image

So the first thing you need to do is write a function that will print out the board on the Python text console. This is the sort of thing that was done before the widespread use of graphics windows, and is a lot simpler to cope with. Basically, you need to output each square on the board but print on a new line when you have outputted squares 2 or 5. This first program is shown in Listing 3.1. Remember, all the code listings for the book can be found at www.wiley.com/go/raspberrypiprojects.

Listing 3.1 Printing the Simple Tic-Tac-Toe Board

#!/usr/bin/env python

# Tic-Tac-Toe 1 - print board

board = [ 'O', 'X', ' ', 'O', ' ', 'X', 'O', 'X', 'X' ]

def printBoard():

print

for square in range(0,9):

print board[square],

if square == 2 or square == 5 :

print

print

if __name__ == '__main__':

printBoard()

This is a simple for loop that prints out the contents of the board list one at a time. The print statement ends in a comma that means that it will not go onto a new line. The variable square will range through all the positions in the list and when it reaches 2 or 5 an extra print statement is executed. This time it is without a comma at the end, so a new line is produced.

A Better Board

Although Listing 3.1 fulfills the desire of printing out the board, it could be improved by adding a grid using some text characters. This is shown in Listing 3.2.

Listing 3.2 Printing a Better Tic-Tac-Toe Board

#!/usr/bin/env python

# Tic-Tac-Toe 2 - print a better board

board = [ 'O', 'X', ' ', 'O', ' ', 'X', 'O', 'X', 'X' ]

def printBoard():

print

print '|',

for square in range(0,9):

print board[square],'|',

if square == 2 or square == 5 :

print

print '- - - - - - -'

print '|',

print

print

if __name__ == '__main__':

printBoard()

This makes the board look a whole lot better by emphasising the blank squares.

Checking for a Win

Next you need a function that checks if the board shows a win for a player. With an eye on what is needed later on, you need to write it so that you pass into the function the player’s symbol that you want to test for. Also you need a list of the squares that constitute a win. In fact there are eight ways to win, so you need a new concept – a list of lists. To see how this works, look at Listing 3.3.

Listing 3.3 Checking the Tic-Tac-Toe Board for a Win

#!/usr/bin/env python

# Tic-Tac-Toe 3 - check for win

board = [ 'O', 'X', ' ', 'O', ' ', 'X', 'O', 'X', 'X' ]

wins = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, image

4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]

def checkWin(player):

win = False

for test in wins :

print test

count = 0

for squares in test :

if board[squares] == player :

count +=1

if count == 3 :

win = True

return win

if __name__ == '__main__':

print 'Checking board for X'

if checkWin('X'):

print 'Game over X wins'

print 'Checking board for O'

if checkWin('O'):

print 'Game over O wins'

The list called wins consists of eight items, and each item is a list of three of the square numbers that constitute a win. The way you drive this is with the loop variables in the for statements. The first statement

for test in wins :

makes the variable called test take on the value of each of the sublists in turn. Then, when it comes to the for statement

for squares in test :

which is nested inside the first one, the variable squares will take on the values, in turn, of the squares that you need to fill with one symbol to get a win. What happens now is that if the board’s symbol matches the one you are looking for, a count is incremented. If that count reaches a value of three, the board has a win for that player. The logic variable win is set initially to false, but if a win is detected, it is changed to true. It is this variable that is returned to the calling program. The last part of the listing simply exercises the checkWin() function by calling it to check for each playing symbol in turn. For the way the board list is set up, there is a win for the O player. You can change the board list to have a win for the X player if you like.

More Infrastructure

The last pieces of infrastructure you need are a function to clear the board for a fresh game and one that checks if any new moves can be made. These are quite simple and are shown in Listing 3.4.

Listing 3.4 Utility Functions

def wipeBoard() :

global board

for square in range(0, len(board) ) :

board[square] = ' '

def canMove(): # see if move can be made

move = False

for square in range(0, len(board) ) :

if board[square] == ' ':

move = True

return move

This just shows the function itself without any test harness. Test harness is the software term used by software engineers to describe temporary code that exists only to test functions in isolation. It is an important part of the process of developing a project to be able to test functions in isolation from the rest of the code. This is because you can then feed the function with all sorts of conditions that might not happen very often when the function is run as part of the normal code.

The wipeBoard function simply puts spaces in every position in the board. Here the statement

for square in range(0, len(board) ) :

generates the loop variable square as just a sequence of numbers. Rather than just put the last number in the range as 16, you have used the len() function, which works out the length of the list. This means that it will work with any size board and hence any length list.

You might think the canMove() function is not necessary, but one situation that checkWin() cannot cope with is when there are no more spaces left on the board – which is why you need canMove(). Initially the move variable is set to be false. Then it visits each square on the board; if it sees a blank space, it sets the move variable to true. It doesn’t matter if this variable is set to be true once or fifteen times, and it is often simpler in programming to let things run on like this rather than try to stop when you find the first instance of a blank square.

A Two-Player Game

Now it is time to put the code pieces together and make a program that can run a two-player game. That is, there are two human players, each taking a turn to enter his or her move. The computer keeps track of the moves, prints out the board after each move and checks if one player has won or if it is a draw. There is, however, just one more thing you need to consider before proceeding.

When representing the board as a list, the elements of the list, as always, start off with the first item position number of zero. Most noncomputer people are not happy with a number zero, so as a sop to them you shall be representing the positions on the board as numbers 1 to 9. Therefore, whenever a number is input from a player, the program has to adjust this number to match the internal representation of the position. True, this involves only adding or subtracting one from the number, but you need to remember to do this each time a number is presented to a player. As a reminder of the numbering system the board list is initialised to represent the number of each square before being wiped prior to beginning the game. This is shown in the two-player version of the game, shown in Listing 3.5.

Listing 3.5 Two-Player Tic-Tac-Toe Game

#!/usr/bin/env python

# Tic-Tac-Toe 5 - 2 player game

board = [ '1', '2', '3', '4', '5', '6', '7', '8', '9' ]

wins = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], image

[1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]

def play() :

printBoard()

print 'Tic-Tac-Toe'

print 'two players'

while True :

wipeBoard()

player_turn = 'X'

while checkWin(swapPlayer(player_turn)) == False image

and canMove() == True :

getMove(player_turn)

printBoard()

player_turn = swapPlayer(player_turn)

if checkWin(swapPlayer(player_turn)):

print 'Player',swapPlayer(player_turn),'wins image

... New Game'

else:

print 'A draw. ... New game'

def swapPlayer(player):

if player == 'X' :

player = 'O'

else:

player = 'X'

return player

def getMove(player):

global board

correct_number = False

while correct_number == False :

square = raw_input('Square to place the '+ player + ' ')

try:

square = int(square)

except:

square = -2

square -= 1 # make input number match internal numbers

if square >= 0 and square < 10 : # number in range

if board[square] == ' ' : # if it is blank

board[square] = player

correct_number = True

else:

print 'Square already occupied'

else :

print 'incorrect square try again'

def wipeBoard() :

global board

for square in range(0, len(board) ) :

board[square] = ' '

def printBoard():

print

print '|',

for square in range(0,9):

print board[square],'|',

if square == 2 or square == 5 :

print

print '- - - - - - -'

print '|',

print

print

def canMove(): # see if move can be made

move = False

for square in range(0, len(board) ) :

if board[square] == ' ':

move = True

return move

def checkWin(player):

win = False

for test in wins :

count = 0

for squares in test :

if board[squares] == player :

count +=1

if count == 3 :

win = True

return win

if __name__ == '__main__':

play()

This contains three functions you have not seen before – play(), swapPlayer() and getMove(). Look at the simplest one first, swapPlayer(): All this does is return a player’s symbol, which is not the symbol passed into it. Although this might seem like a trivial thing to do, this action is required at many different parts of the code, which is why it warrants being built into a function.

The getMove() function might look more complex than you might expect. This is because there is a lot of code designed to make sure that a valid number is entered by the user. If you could trust the user to enter a valid square number, this would all be unnecessary – but you can’t, so it is necessary.

Chapter 2 uses the raw_input function but only to input a name; here you want to input a number. So first you have to check that it is a number that has been entered and then that the number represents a square that doesn’t already have a symbol in it. Therefore the structure used in this function is a while loop that keeps repeating until a valid number has been entered. After getting something from the user you must check that it is a number; this can be done with the int() function, which turns a text input into a number (an integer in this case). If this can’t be done because the user has typed in some alphabetical or punctuation characters, the program will crash and print out an error message, and you don’t want that to happen. Therefore you use a new function structure try, which allows the program to check if the code is going to throw an error; if so, the program jumps to the except label, where you set the variable to an out-of-range number so that it can be rejected later on in the function. Then you test if the variable is within the range of the board. Finally, you check to see if that space is actually free to take a symbol. Only when this final test is passed do you actually make the move on the board and set the variable correct_number to true, which stops the while loop from repeating. Otherwise, a hopefully helpful error message is relayed to the user.

The play() function orchestrates the whole game. First the board is printed out, which, as mentioned before, contains the square numbering identification information. Then you enter an endless loop which repeatedly plays a game. It starts off by wiping the board and setting the first player as X. The individual game is contained in the while loop that keeps on going until there is a winner or there are no more blank squares to use. The player’s move is then made and the board printed again, and then you swap the player for the next move. The code then loops around to thewhile function, but as you have already swapped the player in anticipation of the next round, when you call the checkWin() function as part of the while tests you must call it for the player who has just gone – hence the use of the swap_player() function in the calling parameter of thecheckWin() function. When one player has won or the free squares have run out, the while loop stops, and the code tests for a win; if a win is found, the winning player is printed. However, if there is not a win, the game must have been a draw, so that is printed out. Then a new game is started. When the users want to quit the game they should press the Ctrl and C keys together.

Getting the Computer to Play

While you now have the computer keeping track of the game, it is not much more than a glorified piece of paper. The fun starts when you get the computer to generate moves in place of one of the players. There are many levels you can do this at, ranging from the computer giving you a poor playing opponent to one of the computer being invincible. Of course, playing a computer at each end of this range is not as much fun as playing one somewhere in the middle. In this section you will look at several ways you can make the computer an increasingly skillful opponent. Because the behaviour that programs like this exhibit often looks like there is some intelligence at play, this sort of programming is often known as artificial intelligence, or AI for short. Although I regularly maintain that artificial intelligence is the ability some students have of passing exams without actually understanding very much.

The Computer As a Five Year Old

In the first level of AI, the computer knows the rules but not much else. It can play the game but has no strategy and can only make a winning move by chance. To do this, the computer must gather a list of valid moves and choose one at random. Functions to do this are shown in Listing 3.6.

Listing 3.6 A Computer Opponent Tic-Tac-Toe Game

#!/usr/bin/env python

# Tic-Tac-Toe 6 - play against the computer random move

from random import shuffle

def play() :

global board

print 'Tic-Tac-Toe'

print 'play against the computer AI level 0'

printBoard()

while True :

wipeBoard()

player_turn = 'X'

while checkWin(swapPlayer(player_turn),board) == False image

and canMove() == True :

if player_turn == 'X':

getMove(player_turn)

else:

generateMove()

printBoard()

player_turn = swapPlayer(player_turn)

if checkWin(swapPlayer(player_turn),board):

print 'Player',swapPlayer(player_turn),'wins image

... New Game'

else:

print 'A draw. ... New game'

def generateMove():

global board

moves = list()

for squares in range(0, len(board) ):

if board[squares] == ' ' :

moves.append(squares)

shuffle(moves)

board[moves[0]] = 'O'

print 'My move is ',moves[0] +1

Note here that I have only shown the first three functions as all the other functions remain the same. The first thing to spot is the use of the random library

from random import shuffle

This imports the random function shuffle, which you will use to rearrange a list of valid moves into a random order so that the game played by the computer varies. The play function is much as before, but where it differs is when it is player O’s move – then the generateMove() function is called. This function starts off by defining a list called moves, which at first is empty. Then a for loop visits every square on the board, and if a square is blank it appends the square’s number to the moves list. The shuffle function is used on the list to get it into a random order, and, as you are only interested in using one move you select the first number in that list, the one at position zero. Next, the move is actually made, and the board is changed. Finally the move number is printed out. Note here that the number printed out is one more than the square’s number because, as mentioned before, you are representing the squares for the user as 1 to 9 whereas the computer sees them as 0 to 8.

Teaching the Computer to Recognize a Winning Move

So whereas Listing 3.6 plays an entertaining game, it is all very hit and miss; when you play it it feels more like miss than hit. Therefore what you want to do is add a bit of strategy to the computer’s play. It would be good if the computer could spot a move that would cause it to win. This is quite easily achieved by looking at the results of making the move in each blank square in turn, and seeing if a win results. As you already have a function that checks for a win this is not too difficult. Similarly, it would be good if the computer could spot that its opponent is able to make a winning move and if so then move in that place to block them. After that you can just let the computer pick a random move like before. The new and changed functions to do this are shown in Listing 3.7.

Listing 3.7 Computer Opponent Level 1 Tic-Tac-Toe Game

#!/usr/bin/env python

# Tic-Tac-Toe 7 - play against the computer AI level 1

def generateMove():

if canIwin():

pass

elif canYouWin():

pass

else:

randomMove()

def randomMove():

global board

moves = list()

for squares in range(0, len(board) ):

if board[squares] == ' ' :

moves.append(squares)

shuffle(moves)

board[moves[0]] = 'O'

print 'My move is ',moves[0] + 1

def canIwin():

global board

testBoard = board

moveMade = False

for square in range(0, len(board) ) :

if testBoard[square] == ' ' and moveMade == False:

testBoard[square] = 'O'

if checkWin('O',testBoard):

board[square] = 'O'

moveMade = True

print 'My move is ',square + 1

else:

testBoard[square] = ' ' # retract move

return moveMade

def canYouWin():

global board

testBoard = board

moveMade = False

for square in range(0, len(board) ) :

if testBoard[square] == ' ' and moveMade == False:

testBoard[square] = 'X'

if checkWin('X',testBoard):

board[square] = 'O'

moveMade = True

print 'My move is ',square + 1

else:

testBoard[square] = ' ' # retract move

return moveMade

Again, I am just showing the functions that have changed. This time the generateMove() function is a little more structured. It first calls the canIwin() function as part of an if statement; when this returns true there is nothing else you want to do. However, Python will throw an error if there is no line there, so you must use the dummy statement pass. As its name implies pass does nothing but tells the computer specifically you want to do nothing and that you haven’t just made a mistake. However, if the function returns false – that is, it cannot win – it then calls thecanYouWin() function. Note here the elif keyword, a concatenation of else if, which means it is an if statement that controls whether the line following it gets executed. In the event of none of these two functions producing a true value, the final else calls the randomMove() function, which contains the code you used last time for generating a valid, but random, move.

The canIwin() function itself first makes a copy of the board, and then visits every square and puts the O symbol in it. After that, it checks for a win. If this returns as true, that move is made to the real board, and the moveMade variable is set to true to stop it from making any more moves. Finally, the move that has just been made is printed out. If the trial move returns false, the move is retracted by placing a blank in the place where the O was just tried. If you look now at the canYouWin() function, this is just the opposite; it scans the board, placing an X in each blank square. If it finds that one of these trial moves will result in a win for the human opponent, it uses this position to place its O, thus blocking the winning move. If you examine the code you wrote for those two functions, you will see that they are nearly identical. When you find this happening it is good to shorten the code by consolidating the two functions into one. You will see that the main difference is the symbol being used. When this happens, as a general rule you make the symbol into a variable, enclose one of the functions in a for loop and use the loop index to switch over the variable. This is shown in Listing 3.8; give it a new, more descriptive name of win_block().

Listing 3.8 Consolidating the Two Win Check Functions

def generateMove():

if win_block():

pass

else:

randomMove()

def win_block(): #move to win or block

global board

testBoard = board

players = ['O','X']

moveMade = False

for moveTry in players:

for square in range(0, len(board) ) :

if testBoard[square] == ' ' and moveMade == False:

testBoard[square] = moveTry

if checkWin(moveTry,testBoard):

board[square] = 'O'

moveMade = True

print 'My move is ',square + 1

else:

testBoard[square] = ' ' # retract move

return moveMade

You will see that the name is changed, as is the code in the generateMove() function. All the other functions remain unchanged. The main addition to the function is the extra list named players, which contains the two symbols to use. Then the extra for loop selects one of these symbols on each pass by the variable moveTry.

The section “Errors” at the start of this chapter discusses logical errors; developing the win_block function initially produced one of these errors. When I wrote the first draft of this function, the loop that changed what symbol was being used was placed on the other side of the next for loop. This actually appeared to work until I spotted that sometimes the program would ignore a winning move in favor of blocking the opponent. It turned out that this was because each square was being tested first – for the computer’s symbol and then for the opponent’s. This meant that if the computer found a blocking move first it would choose that. Therefore, I needed to move the for moveTry statement to its current position so that all the winning moves were scanned before the blocking moves were looked at.

Adding Some Strategy

At this point the game is being played with a modicum of intelligence but still no strategy. You need to devise a playing strategy to use. A strong one but by no means the only one is shown in Figure 3.2, which shows the sort of choices that are made when picking a move. Basically after checking for a winning or blocking move, if there are corners free, then pick one, or if the centre is free, pick it; finally, if none of the previous conditions applies, pick one of the sides.

Figure 3-2: A flow diagram of move choice.

image

Flow diagrams are a bit out of favor nowadays, but as their name implies, they are good for showing the flow of an algorithm and the decisions that have to be made. One problem is that it can be much harder, with today’s programming languages and structures, to translate the flow directly into code. However, for beginners I still feel there is great merit in these diagrams, especially relatively short flow diagrams. Anything in a diamond requires a decision, and there are two or more possible changes in the flow depending on that decision. Now these sorts of statements are fine when you are describing the strategy to another human being, but when you write code you have to be more specific. The way I have decided to implement this is to write a function that is given a list of possible moves; the function will check this list to see if the board is free to take one or more of these moves, and pick a random move from the list if it is. Most of the logic to do this was used in the randomMove() function. The new functions to do this are shown in Listing 3.9.

Listing 3.9 An Intelligent Move Strategy

def generateMove():

if win_block():

pass

elif prefMove([0,2,6,8]): # corners

pass

elif prefMove([5]): # centre

pass

else:

prefMove([1,3,5,7]) # middle row

def prefMove(moves):

global board

moved = False

move = list()

for potential in moves:

if board[potential] == ' ':

move.append(potential)

if len(move) != 0:

shuffle(move)

board[move[0]] = 'O'

print 'My move is ',move[0] + 1

moved = True

return moved

Again I have only shown the functions that have changed. The big change this time is the prefMove() function. This is passed a list of preferred moves from the generateMoves() function; notice this is a list, and it is given in square brackets inside the curved brackets. After thewin_block() function is called, a list of all the corners is passed. If that does not result in a move, a list of all the centre squares is passed. Of course, there is only one centre square, so it is a short list, but the important thing is that it is a list, and the same logic can operate on it all the same. Finally a list of squares in the middle of each row is passed to the prefMove() function. This works just like the randomMove() function, which is no longer needed, in that it examines the state of the board for each square in the list, and compiles a list of free squares – that is, ones without symbols already in them. Then prefMove shuffles the list and chooses the first element. This produces a good game but one that is possible to beat if the random moves chosen are not the right ones. Nevertheless, it is fun to play and good practice in developing the playing strategy even further.

Refining the Computer’s Strategy

Because the game of tic-tac-toe is relatively short, it turns out that the first move the computer makes is vital in its success, so in this next version you refine the move choice for the first move only. Basically, if the human player opened by playing a corner, the computer must respond with the opposite corner if it is to prevent the human from winning. As it is, sometimes this happens by the random choice of corner, but not always. Also, if the human opens with a middle row square, the response should be an adjacent corner square. The whole listing, with all the functions in it this time, is shown in Listing 3.10.

Listing 3.10 An Improved Computer Playing Strategy

#!/usr/bin/env python

# Tic-Tac-Toe 10 - play against the computer AI level 3

from random import shuffle

board = [ '1', '2', '3', '4', '5', '6', '7', '8', '9' ]

wins = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], image

[1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]

def play() :

global board

print 'Tic-Tac-Toe'

print 'play against the computer AI level 3'

printBoard()

while True :

wipeBoard()

move = 0

player_turn = 'X'

while checkWin(swapPlayer(player_turn),board) == image

False and canMove() == True :

if player_turn == 'X':

getMove(player_turn)

move +=1

else:

generateMove(move)

printBoard()

player_turn = swapPlayer(player_turn)

if checkWin(swapPlayer(player_turn),board):

print 'Player',swapPlayer(player_turn),'wins image

... New Game'

else:

print 'A draw. ... New game'

def generateMove(move):

corners = [0, 2, 6, 8]

opposite = [8, 6, 2, 0]

side = [1, 3, 5, 7]

adjacent = [0, 6, 2, 8]

if move == 1:

moved = False

for square in range(0,4):

if board[corners[square]] == 'X':

moved = prefMove([opposite[square]])

for square in range(0,4):

if board[side[square]] == 'X':

moved = prefMove([adjacent[square]])

if not moved :

prefMove([0,2,6,8]) # corners

else:

if win_block():

pass

elif prefMove([0,2,6,8]): # corners

pass

elif prefMove([5]): # centre

pass

else:

prefMove([1,3,5,7]) # middle row

def prefMove(moves):

global board

moved = False

move = list()

for potential in moves:

if board[potential] == ' ':

move.append(potential)

if len(move) != 0:

shuffle(move)

board[move[0]] = 'O'

print 'My move is ',move[0] + 1

moved = True

return moved

def win_block(): #move to win or block

global board

testBoard = board

players = ['O','X']

moveMade = False

for moveTry in players:

for square in range(0, len(board) ) :

if testBoard[square] == ' ' and moveMade == False:

testBoard[square] = moveTry

if checkWin(moveTry,testBoard):

board[square] = 'O'

moveMade = True

print 'My move is ',square + 1

else:

testBoard[square] = ' ' # retract move

return moveMade

def swapPlayer(player):

if player == 'X' :

player = 'O'

else:

player = 'X'

return player

def getMove(player):

global board

correct_number = False

while correct_number == False :

square = raw_input('Square to place the '+ player + ' ')

try:

square = int(square)

except:

square = -2

square -= 1 # make input number image

match internal representation

if square >= 0 and square < 10 : # number in range

if board[square] == ' ' : # if it is blank

board[square] = player

correct_number = True

else:

print 'Square already occupied'

else :

print 'incorrect square try again'

def wipeBoard() :

global board

for square in range(0, len(board) ) :

board[square] = ' '

def printBoard():

print

print '|',

for square in range(0,9):

print board[square],'|',

if square == 2 or square == 5 :

print

print '- - - - - - -'

print '|',

print

print

def canMove(): # see if move can be made

move = False

for square in range(0, len(board) ) :

if board[square] == ' ':

move = True

return move

def checkWin(player,board):

win = False

for test in wins :

count = 0

for squares in test :

if board[squares] == player :

count +=1

if count == 3 :

win = True

return win

if __name__ == '__main__':

play()

Here, the only changes from the earlier listings are to the play() and generateMove() functions, but the program is shown in its entirety. The play() function is changed to keep a count of the number of moves that have been made, and this number is passed to the generateMove()function. If the move number is anything other than one, the move-picking strategy is the same as before. However, if it is the first move the computer has made, it responds in a different way. There are four lists containing the corners and the corresponding opposite corners, along with the side squares and the corresponding adjacent squares to play in response. Note that these adjacent squares are also corners, but this is of no importance here. Also, every side square has two adjacent corner squares, but it doesn’t matter which one is picked. Finally, if the human player opens with the centre square, the computer just picks one of the corners.

The way this works is that for each move and its response, a list is generated. Then the move list is stepped through one square at a time, looking for the human player’s symbol in each square. If it is found, the corresponding element of the response list is passed to the prefMove() function. As this element is a list whose length is only one, it gets picked. There is no point writing a new function when an existing function will suffice. The same is done for the side moves. Note as this only is performed on the first move, there is only one X symbol to find. This plays a pretty mean game that I think can’t be beaten.

Over to You

Well, it is now over to you; it’s time to add your own improvements and embellishments. You might want to improve on how the board looks by printing more characters and giving it more space. On the other hand, you might want it to print quickly and revert to the simpler board you started with. You might not want to print out the board after each move, but only when both you and the computer have moved. The numbering of the board is conventional, but perhaps you want to change this to match the locations of the numbers on the numeric keypad on your keyboard – that is, if you have one. That would mean the top-left square would be 7, and lower-right one would be 3. You can change it in the input section simply by using a list as a lookup table; don’t forget to also use another lookup table to translate when the computer prints out its move. (A lookup table is a predefined conversion between one number and another. It is simply a list where the input value is the index of the list and the output value; the one you want to associate with the input value is the contents of that list element.)

You will notice that the computer is polite, and it always allows you to go first. Look into making changes to allow the computer to play first. I feel that X should always go first, but you might look into offering a choice of symbols to the opening player as well. You can try and change the playing strategy into giving other classes of squares first priority in place of the corners. If you are more experienced, you might like to look up the minimax algorithm that computes all possible moves. To do that, you have to use a recursive algorithm, which involves a function that calls itself. These sorts of things can be very powerful but difficult to get your head around. Perhaps that is too much at this stage; you might be better off leaving that until you have finished the book.