The Reversegam AI algorithm from Chapter 15 is simple, but it beats me almost every time I play it. Because the computer can process instructions quickly, it can easily check each possible position on the board and select the highest-scoring move. It would take me a long time to find the best move this way.
The Reversegam program had two functions, getPlayerMove() and getComputerMove(), which both returned the move selected as a two-item list in the format [x, y]. Both functions also had the same parameters, the game board data structure and one type of tile, but the returned moves came from different sources—either the player or the Reversegam algorithm.
What happens when we replace the call to getPlayerMove() with a call to getComputerMove()? Then the player never has to enter a move; it is decided for them. The computer is playing against itself!
In this chapter, we’ll make three new programs in which the computer plays against itself, each based on the Reversegam program in Chapter 15:
• Simulation 1: AISim1.py will make changes to reversegam.py.
• Simulation 2: AISim2.py will make changes to AISim1.py.
• Simulation 3: AISim3.py will make changes to AISim2.py.
The small changes from one program to the next will show you how to turn a “player versus computer” game into a “computer versus computer” simulation. The final program, AISim3.py, shares most of its code with reversegam.py but serves quite a different purpose. The simulation doesn’t let us play Reversegam but teaches us more about the game itself.
You can either type in these changes yourself or download them from the book’s website at https://www.nostarch.com/inventwithpython/.
Our AISim1.py program will have a few simple changes so that the computer plays against itself. Both the getPlayerMove() and getComputerMove() functions take a board data structure and the player’s tile, and then return the move to make. This is why getComputerMove() can replace getPlayerMove() and the program still works. In the AISim1.py program, the getComputerMove() function is being called for both the X and O players.
We also make the program stop printing the game board for moves that are made. Since a human can’t read the game boards as fast as a computer makes moves, it isn’t useful to print every move, so we just print the final board at the end of the game.
These are just minimal changes to the program, so it will still say things like The player will go first. even though the computer is playing as both the computer and the player.
Here’s what the user sees when they run the AISim1.py program. The text entered by the player is bold.
Welcome to Reversegam!
The computer will go first.
12345678
+--------+
1|XXXXXXXX|1
2|OXXXXXXX|2
3|XOXXOXXX|3
4|XXOOXOOX|4
5|XXOOXXXX|5
6|XXOXOXXX|6
7|XXXOXOXX|7
8|XXXXXXXX|8
+--------+
12345678
X scored 51 points. O scored 13 points.
You beat the computer by 38 points! Congratulations!
Do you want to play again? (yes or no)
no
Save the old reversegam.py file as AISim1.py as follows:
Select File Save As.
Save this file as AISim1.py so that you can make changes without affecting reversegam.py. (At this point, reversegam.py and AISim1.py still have the same code.)
Make changes to AISim1.py and save that file to keep any changes. (AISim1.py will have the new changes, and reversegam.py will have the original, unchanged code.)
This process will create a copy of our Reversegam source code as a new file that you can make changes to, while leaving the original Reversegam game the same (you may want to play it again to test it). For example, change line 216 in AISim1.py to the following (the change is in bold):
216. move = getComputerMove(board, playerTile)
Now run the program. Notice that the game still asks whether you want to be X or O, but it won’t ask you to enter any moves. When you replace the getPlayerMove() function with the getComputerMove() function, you no longer call any code that takes this input from the player. The player still presses ENTER after the original computer’s moves (because of the input('Press Enter to see the computer\'s move.') on line 232), but the game plays itself!
Let’s make some other changes to AISim1.py. Change the following bolded lines. The changes start at line 209. Most of these changes are simply commenting out code, which means turning the code into a comment so it won’t run.
If you get errors after typing in this code, compare the code you typed to the book’s code with the online diff tool at https://www.nostarch.com/inventwithpython#diff.
AISim1.py
207. elif turn == 'player': # Player's turn
208. if playerValidMoves != []:
209. #if showHints:
210. # validMovesBoard = getBoardWithValidMoves(board,
playerTile)
211. # drawBoard(validMovesBoard)
212. #else:
213. # drawBoard(board)
214. #printScore(board, playerTile, computerTile)
215.
216. move = getComputerMove(board, playerTile)
217. #if move == 'quit':
218. # print('Thanks for playing!')
219. # sys.exit() # Terminate the program.
220. #elif move == 'hints':
221. # showHints = not showHints
222. # continue
223. #else:
224. makeMove(board, playerTile, move[0], move[1])
225. turn = 'computer'
226.
227. elif turn == 'computer': # Computer's turn
228. if computerValidMoves != []:
229. #drawBoard(board)
230. #printScore(board, playerTile, computerTile)
231.
232. #input('Press Enter to see the computer\'s move.')
233. move = getComputerMove(board, computerTile)
234. makeMove(board, computerTile, move[0], move[1])
235. turn = 'player'
236.
237.
238.
239. print('Welcome to Reversegam!')
240.
241. playerTile, computerTile = ['X', 'O'] #enterPlayerTile()
As you can seen, the AISim1.py program is mostly the same as the original Reversegam program, except we’ve replaced the call to getPlayerMove() with a call to getComputerMove(). We’ve also made some changes to the text that is printed to the screen so that the game easier to follow. When you run the program, the entire game is played in less than a second!
Again, most of the changes are simply commenting out code. Since the computer is playing against itself, the program no longer needs to run code to get moves from the player or display the state of the board. All of this is skipped so that the board is displayed only at the very end of the game. We comment out code instead of deleting it because it’s easier to restore the code by uncommenting it if we need to reuse the code later.
We commented out lines 209 to 214 because we don’t need to draw a game board for the player since they won’t be playing the game. We also commented out lines 217 to 223 because we don’t need to check whether the player enters quit or toggles the hints mode. But we need to de-indent line 224 by four spaces since it was in the else block that we just commented out. Lines 229 to 232 also draw the game board for the player, so we comment out those lines, too.
The only new code is on lines 216 and 241. In line 216, we just substitute the call to getPlayerMove() with getComputerMove(), as discussed earlier. On line 241, instead of asking the player whether they want to be X or O, we simply always assign 'X' to playerTile and 'O' to computerTile. (Both of these players will be played by the computer, though, so you can rename playerTile to computerTile2 or secondComputerTile if you want.) Now that we have the computer playing against itself, we can keep modifying our program to make it do more interesting things.
If we created a new algorithm, we could set it against the AI implemented in getComputerMove() and see which one is better. Before we do so, however, we need a way to evaluate the players. We can’t evaluate which AI is better based on only one game, so we should have the AIs play against each other more than once. To do that, we’ll make some changes to the source code. Follow these steps to make AISim2.py:
Select File Save As.
Save this file as AISim2.py so that you can make changes without affecting AISim1.py. (At this point, AISim1.py and AISim2.py still have the same code.)
Here’s what the user sees when they run the AISim2.py program.
Welcome to Reversegam!
#1: X scored 45 points. O scored 19 points.
#2: X scored 38 points. O scored 26 points.
#3: X scored 20 points. O scored 44 points.
#4: X scored 24 points. O scored 40 points.
#5: X scored 8 points. O scored 56 points.
--snip--
#249: X scored 24 points. O scored 40 points.
#250: X scored 43 points. O scored 21 points.
X wins: 119 (47.6%)
O wins: 127 (50.8%)
Ties: 4 (1.6%)
Because the algorithms include randomness, your run won’t have exactly the same numbers.
Change the code in AISim2.py to match the following. Make sure that you change the code line by line in number order. If you get errors after typing in this code, compare the code you typed to the book’s code with the online diff tool at https://www.nostarch.com/inventwithpython#diff.
AISim2.py
235. turn = 'player'
236.
237. NUM_GAMES = 250
238. xWins = oWins = ties = 0
239. print('Welcome to Reversegam!')
240.
241. playerTile, computerTile = ['X', 'O'] #enterPlayerTile()
242.
243. for i in range(NUM_GAMES): #while True:
244. finalBoard = playGame(playerTile, computerTile)
245.
246. # Display the final score.
247. #drawBoard(finalBoard)
248. scores = getScoreOfBoard(finalBoard)
249. print('#%s: X scored %s points. O scored %s points.' % (i + 1,
scores['X'], scores['O']))
250. if scores[playerTile] > scores[computerTile]:
251. xWins += 1 #print('You beat the computer by %s points!
Congratulations!' % (scores[playerTile] -
scores[computerTile]))
252. elif scores[playerTile] < scores[computerTile]:
253. oWins += 1 #print('You lost. The computer beat you by %s points.'
% (scores[computerTile] - scores[playerTile]))
254. else:
255. ties += 1 #print('The game was a tie!')
256.
257. #print('Do you want to play again? (yes or no)')
258. #if not input().lower().startswith('y'):
259. # break
260.
261. print('X wins: %s (%s%%)' % (xWins, round(xWins / NUM_GAMES * 100, 1)))
262. print('O wins: %s (%s%%)' % (oWins, round(oWins / NUM_GAMES * 100, 1)))
263. print('Ties: %s (%s%%)' % (ties, round(ties / NUM_GAMES * 100, 1)))
If this is confusing, you can always download the AISim2.py source code from the book’s website at https://www.nostarch.com/inventwithpython/.
The main information we want from the simulation is how many wins for X, wins for O, and ties there are over a certain number of games. These can be tracked in four variables, which are created on lines 237 and 238.
237. NUM_GAMES = 250
238. xWins = oWins = ties = 0
The constant NUM_GAMES determines how many games the computer will play. You’ve added the variables xWins, oWins, and ties to keep track of when X wins, when O wins, and when they tie. You can chain the assignment statement together to set ties equal to 0 and oWins equal to ties, then xWins equal to oWins. This sets all three variables to 0.
NUM_GAMES is used in a for loop that replaces the game loop on line 243:
243. for i in range(NUM_GAMES): #while True:
The for loop runs the game the number of times in NUM_GAMES. This replaces the while loop that used to loop until the player said they didn’t want to play another game.
At line 250, an if statement compares the score of the two players, and lines 251 to 255 in the if-elif-else blocks increment the xWins, oWins, and ties variables at the end of each game before looping back to start a new game:
250. if scores[playerTile] > scores[computerTile]:
251. xWins += 1 #print('You beat the computer by %s points!
Congratulations!' % (scores[playerTile] -
scores[computerTile]))
252. elif scores[playerTile] < scores[computerTile]:
253. oWins += 1 #print('You lost. The computer beat you by %s points.'
% (scores[computerTile] - scores[playerTile]))
254. else:
255. ties += 1 #print('The game was a tie!')
We comment out the messages originally printed in the block so now only a one-line summary of the scores prints for each game. We’ll use the xWins, oWins, and ties variables later in the code to analyze how the computer performed against itself.
You also commented out lines 247 and 257 to 259. By doing that, you took out most of the print() function calls from the program, as well as the calls to drawBoard(). We don’t need to see each of the games since there are so many being played. The program still runs every game in its entirety using the AI we coded, but only the resulting scores are shown. After running all the games, the program shows how many games each side won, and lines 251 to 253 print some information about the game runs.
Printing things to the screen slows the computer down, but now that you’ve removed that code, the computer can run an entire game of Reversegam in about a second or two. Each time the program printed out one of those lines with the final score, it ran through an entire game (checking about 50 or 60 moves individually to choose the one that gets the most points). Now that the computer doesn’t have to do as much work, it can run much faster.
The numbers that the program prints at the end are statistics—numbers that are used to summarize how the games were played. In this case, we showed the resulting scores of each game played and the percentages of wins and ties for the tiles.
Percentages are a portion of a total amount. The percentages of a whole can range from 0 percent to 100 percent. If you had 100 percent of a pie, you would have the entire pie; if you had 0 percent of a pie, you wouldn’t have any pie at all; and if you had 50 percent of the pie, you would have half of it.
We can calculate percentages with division. To get a percentage, divide the part you have by the total and then multiply that by 100. For example, if X won 50 out of 100 games, you would calculate the expression 50 / 100, which evaluates to 0.5. Multiply this by 100 to get the percentage (in this case, 50 percent).
If X won 100 out of 200 games, you would calculate the percentage with 100 / 200, which also evaluates to 0.5. When you multiply 0.5 by 100 to get the percentage, you get 50 percent. Winning 100 out of 200 games is the same percentage (that is, the same portion) as winning 50 out of 100 games.
In lines 261 to 263, we use percentages to print information about the outcomes of the games:
261. print('X wins: %s (%s%%)' % (xWins, round(xWins / NUM_GAMES * 100, 1)))
262. print('O wins: %s (%s%%)' % (oWins, round(oWins / NUM_GAMES * 100, 1)))
263. print('Ties: %s (%s%%)' % (ties, round(ties / NUM_GAMES * 100, 1)))
Each print() statement has a label that tells the user whether the data being printed is for X wins, O wins, or ties. We use string interpolation to insert the number of games won or tied and then insert the calculated percentage the wins or ties make up of the total games, but you can see that we’re not simply dividing the xWins, oWins, or ties by the total games and multiplying by 100. This is because we want to print only one decimal place for each percentage, which we can’t do with normal division.
When you use the division operator (/), the expression will always evaluate to a floating-point number. For example, the expression 10 / 2 evaluates to the floating-point value 5.0, not to the integer value 5.
This is important to remember, because adding an integer to a floating-point value with the + addition operator will also always evaluate to a floating-point value. For example, 3 + 4.0 evaluates to the floating-point value 7.0, not to the integer 7.
Enter the following code into the interactive shell:
>>> spam = 100 / 4
>>> spam
25.0
>>> spam = spam + 20
>>> spam
45.0
In the example, the data type of the value stored in spam is always a floating-point value. You can pass the floating-point value to the int() function, which returns an integer form of the floating-point value. But this will always round the floating-point value down. For example, the expressions int(4.0), int(4.2), and int(4.9) all evaluate to 4, not 5. But in AISim2.py, we need to round each percentage to the tenths place. Since we can’t just divide to do this, we need to use the round() function.
The round() function rounds a floating-point number to the nearest integer number. Enter the following into the interactive shell:
>>> round(10.0)
10
>>> round(10.2)
10
>>> round(8.7)
9
>>> round(3.4999)
3
>>> round(2.5422, 2)
2.54
The round() function also has an optional second parameter, where you can specify what place you want to round the number to. This will make the rounded number a floating-point number rather than an integer. For example, the expression round(2.5422, 2) evaluates to 2.54 and round(2.5422, 3) evaluates to 2.542. In lines 261 to 263 of AISim2.py, we use this round() with a parameter of 1 to find the fraction of games won or tied by X and O up to one decimal place, which gives us accurate percentages.
With just a few changes, we can make the computer play hundreds of games against itself. Right now, each player wins about half of the games, since both run exactly the same algorithm for moves. But if we add different algorithms, we can see whether a different AI will win more games.
Let’s add some new functions with new algorithms. But first, in AISim2.py select File Save As to save this new file as AISim3.py.
We’ll rename the getComputerMove() function to getCornerBestMove(), since this algorithm tries to move on corners first and then chooses the move that flips the most tiles. We’ll call this strategy the corner-best algorithm. We’ll also add several other functions that implement different strategies, including a worst-move algorithm that gets the worst-scoring move; a random-move algorithm that gets any valid move; and a corner-side-best algorithm, which works the same as the corner-best AI except that it looks for a side move after a corner move and before taking the highest-scoring move.
In AISim3.py, the call to getComputerMove() on line 257 will be changed to getCornerBestMove(), and line 274’s getComputerMove() will become getWorstMove(), which is the function we’ll write for the worst-move algorithm. This way, we’ll have the regular corner-best algorithm go against an algorithm that purposefully picks the move that will flip the fewest tiles.
As you enter the source code of AISim3.py into your renamed copy of AISim2.py, make sure to write your code line by line in number order so that the line numbers match. If you get errors after typing in this code, compare the code you typed to the book’s code with the online diff tool at https://www.nostarch.com/inventwithpython#diff.
AISim3.py
162. def getCornerBestMove(board, computerTile):
--snip--
184. def getWorstMove(board, tile):
185. # Return the move that flips the least number of tiles.
186. possibleMoves = getValidMoves(board, tile)
187. random.shuffle(possibleMoves) # Randomize the order of the moves.
188.
189. # Find the lowest-scoring move possible.
190. worstScore = 64
191. for x, y in possibleMoves:
192. boardCopy = getBoardCopy(board)
193. makeMove(boardCopy, tile, x, y)
194. score = getScoreOfBoard(boardCopy)[tile]
195. if score < worstScore:
196. worstMove = [x, y]
197. worstScore = score
198.
199. return worstMove
200.
201. def getRandomMove(board, tile):
202. possibleMoves = getValidMoves(board, tile)
203. return random.choice(possibleMoves)
204.
205. def isOnSide(x, y):
206. return x == 0 or x == WIDTH - 1 or y == 0 or y == HEIGHT - 1
207.
208. def getCornerSideBestMove(board, tile):
209. # Return a corner move, a side move, or the best move.
210. possibleMoves = getValidMoves(board, tile)
211. random.shuffle(possibleMoves) # Randomize the order of the moves.
212.
213. # Always go for a corner if available.
214. for x, y in possibleMoves:
215. if isOnCorner(x, y):
216. return [x, y]
217.
218. # If there is no corner move to make, return a side move.
219. for x, y in possibleMoves:
220. if isOnSide(x, y):
221. return [x, y]
222.
223. return getCornerBestMove(board, tile) # Do what the normal AI
would do.
224.
225. def printScore(board, playerTile, computerTile):
--snip--
257. move = getCornerBestMove(board, playerTile)
--snip--
274. move = getWorstMove(board, computerTile)
Running AISim3.py results in the same kind of output as AISim2.py, except different algorithms will be playing the games.
The functions getCornerBestMove(), getWorstMove(), getRandomMove(), and getCornerSideBestMove() are similar to one another but use slightly different strategies to play games. One of them uses the new isOnSide() function. This is similar to our isOnCorner() function, but it checks for the spaces along the side of the board before selecting the highest-scoring move.
We already have the code for an AI that chooses to move on a corner and then chooses the best move possible, since that’s what getComputerMove() does. We can just change the name of getComputerMove() to something more descriptive, so change line 162 to rename our function to getCornerBestMove():
162. def getCornerBestMove(board, computerTile):
Since getComputerMove() no longer exists, we need to update the code on line 257 to getCornerBestMove():
257. move = getCornerBestMove(board, playerTile)
That’s all the work we need to do for this AI, so let’s move on.
The worst-move AI just finds the move with the fewest-scoring points and returns that. Its code is a lot like the code we used to find the highest-scoring move in our original getComputerMove() algorithm:
184. def getWorstMove(board, tile):
185. # Return the move that flips the least number of tiles.
186. possibleMoves = getValidMoves(board, tile)
187. random.shuffle(possibleMoves) # Randomize the order of the moves.
188.
189. # Find the lowest-scoring move possible.
190. worstScore = 64
191. for x, y in possibleMoves:
192. boardCopy = getBoardCopy(board)
193. makeMove(boardCopy, tile, x, y)
194. score = getScoreOfBoard(boardCopy)[tile]
195. if score < worstScore:
196. worstMove = [x, y]
197. worstScore = score
198.
199. return worstMove
The algorithm for getWorstMove() starts out the same for lines 186 and 187, but then it makes a departure at line 190. We set up a variable to hold the worstScore instead of bestScore and set it to 64, because that is the total number of positions on the board and the most points that could be scored if the entire board were filled. Lines 191 to 194 are the same as the original algorithm, but then line 195 checks whether the score is less than worstScore instead of whether the score is higher. If score is less, then worstMove is replaced with the move on the board the algorithm is currently testing, and worstScore is updated, too. Then the function returns worstMove.
Finally, line 274’s getComputerMove() needs to be changed to getWorstMove():
274. move = getWorstMove(board, computerTile)
When this is done and you run the program, getCornerBestMove() and getWorstMove() will play against each other.
The random-move AI just finds all the valid possible moves and then chooses a random one.
201. def getRandomMove(board, tile):
202. possibleMoves = getValidMoves(board, tile)
203. return random.choice(possibleMoves)
It uses getValidMoves(), just as all the other AIs do, and then uses choice() to return one of the possible moves in the returned list.
Before we get into the algorithms, let’s look at one new helper function we’ve added. The isOnSide() helper function is like the isOnCorner() function, except that it checks whether a move is on the sides of a board:
205. def isOnSide(x, y):
206. return x == 0 or x == WIDTH - 1 or y == 0 or y == HEIGHT - 1
It has one Boolean expression that checks whether the x value or y value of the coordinate arguments passed to it is equal to 0 or 7. Any coordinate with a 0 or a 7 in it is at the edge of the board.
We’ll use this function next in the corner-side-best AI.
The corner-side-best AI works a lot like the corner-best AI, so we can reuse some of the code we’ve already entered. We define this AI in the function getCornerSideBestMove():
208. def getCornerSideBestMove(board, tile):
209. # Return a corner move, a side move, or the best move.
210. possibleMoves = getValidMoves(board, tile)
211. random.shuffle(possibleMoves) # Randomize the order of the moves.
212.
213. # Always go for a corner if available.
214. for x, y in possibleMoves:
215. if isOnCorner(x, y):
216. return [x, y]
217.
218. # If there is no corner move to make, return a side move.
219. for x, y in possibleMoves:
220. if isOnSide(x, y):
221. return [x, y]
222.
223. return getCornerBestMove(board, tile) # Do what the normal AI
would do.
Lines 210 and 211 are the same as in our corner-best AI, and lines 214 to 216 are identical to our algorithm to check for a corner move in our original getComputerMove() AI. If there’s no corner move, then lines 219 to 221 check for a side move by using the isOnSide() helper function. Once all corner and side moves have been checked for availability, if there’s still no move, then we reuse our getCornerBestMove() function. Since there were no corner moves earlier and there still won’t be any when the code reaches the getCornerBestMove() function, this function will just look for the highest-scoring move to make and return that.
Table 16-1 reviews the new algorithms we’ve made.
Table 16-1: Functions Used for the Reversegam AIs
Function |
Description |
getCornerBestMove() |
Take a corner move if available. If there’s no corner, find the highest-scoring move. |
getCornerSideBestMove() |
Take a corner move if available. If there’s no corner, take a space on the side. If no sides are available, use the regular getCornerBestMove() algorithm. |
getRandomMove() |
Randomly choose a valid move to make. |
getWorstMove() |
Take the position that will result in the fewest tiles being flipped. |
Now that we have our algorithms, we can pit them against each other.
We’ve written our program so that the corner-best AI plays against the worst-move AI. We can run the program to simulate how well the AIs do against each other and analyze the results with the printed statistics.
In addition to these two AIs, we’ve made some others that we don’t call on. These AIs exist in the code but aren’t being used, so if we want to see how they fare in a match, we’ll need to edit the code to call on them. Since we already have one comparison set up, let’s see how the worst-move AI does against the corner-best AI.
Run the program to pit the getCornerBestMove() function against the getWorstMove() function. Unsurprisingly, the strategy of flipping the fewest tiles each turn will lose most games:
X wins: 206 (82.4%)
O wins: 41 (16.4%)
Ties: 3 (1.2%)
What is surprising is that sometimes the worst-move strategy does work! Rather than winning 100 percent of the time, the algorithm in the getCornerBestMove() function wins only about 80 percent of the time. About 1 in 5 times, it loses!
This is the power of running simulation programs: you can find novel insights that would take much longer for you to realize if you were just playing games on your own. The computer is much faster!
Let’s try a different strategy. Change the getWorstMove() call on line 274 to getRandomMove():
274. move = getRandomMove(board, computerTile)
When you run the program now, it will look something like this:
Welcome to Reversegam!
#1: X scored 32 points. O scored 32 points.
#2: X scored 44 points. O scored 20 points.
#3: X scored 31 points. O scored 33 points.
#4: X scored 45 points. O scored 19 points.
#5: X scored 49 points. O scored 15 points.
--snip--
#249: X scored 20 points. O scored 44 points.
#250: X scored 38 points. O scored 26 points.
X wins: 195 (78.0%)
O wins: 48 (19.2%)
Ties: 7 (2.8%)
The random-move algorithm getRandomMove() did slightly better against the corner-best algorithm than did the worst-move algorithm. This makes sense because making intelligent choices is usually better than just choosing moves at random, but making random choices is slightly better than purposefully choosing the worst move.
Picking a corner space if it’s available is a good idea, because a tile on the corner can never be flipped. Putting a tile on the side spaces seems like it might also be a good idea, since there are fewer ways it can be surrounded and flipped. But does this benefit outweigh passing up moves that would flip more tiles? Let’s find out by pitting the corner-best algorithm against the corner-side-best algorithm.
Change the algorithm on line 274 to use getCornerSideBestMove():
274. move = getCornerSideBestMove(board, computerTile)
Then run the program again:
Welcome to Reversegam!
#1: X scored 27 points. O scored 37 points.
#2: X scored 39 points. O scored 25 points.
#3: X scored 41 points. O scored 23 points.
--snip--
#249: X scored 48 points. O scored 16 points.
#250: X scored 38 points. O scored 26 points.
X wins: 152 (60.8%)
O wins: 89 (35.6%)
Ties: 9 (3.6%)
Wow! That’s unexpected. It seems that choosing the side spaces over a space that flips more tiles is a bad strategy. The benefit of the side space doesn’t outweigh the cost of flipping fewer of the opponent’s tiles. Can we be sure of these results? Let’s run the program again, but this time play 1,000 games by changing line 278 to NUM_GAMES = 1000 in AISim3.py. The program may now take a few minutes for your computer to run—but it would take weeks for you to do this by hand!
You’ll see that the more accurate statistics from the 1,000-games run are about the same as the statistics from the 250-games run. It seems that choosing the move that flips the most tiles is a better idea than choosing a side to move on.
We’ve just used programming to find out which game strategy works the best. When you hear about scientists using computer models, this is what they’re doing. They use a simulation to re-create some real-world process, and then do tests in the simulation to find out more about the real world.
This chapter didn’t cover a new game, but it modeled various strategies for Reversegam. If we thought that taking side moves in Reversegam was a good idea, we would have to spend weeks, even months, carefully playing games of Reversegam by hand and writing down the results to test this idea. But if we know how to program a computer to play Reversegam, then we can have it try different strategies for us. If you think about it, the computer is executing millions of lines of our Python program in seconds! Your experiments with the simulations of Reversegam can help you learn more about playing it in real life.
In fact, this chapter would make a good science fair project. You could research which set of moves leads to the most wins against other sets of moves, and you could make a hypothesis about which is the best strategy. After running several simulations, you could determine which strategy works best. With programming, you can make a science fair project out of a simulation of any board game! And it’s all because you know how to instruct the computer to do it, step by step, line by line. You can speak the computer’s language and get it to do large amounts of data processing and number crunching for you.
That’s all for the text-based games in this book. Games that use only text can be fun, even though they’re simple. But most modern games use graphics, sound, and animation to make them more exciting. In the rest of the chapters in this book, you’ll learn how to create games with graphics by using a Python module called pygame.