10
Tic-Tac-Toe

In this chapter, you’ll build a voice-controlled tic-tac-toe game to put all your new skills into practice. You’ll draw a game board with blue and white game pieces, disallow invalid moves, and detect if a player has won. You’ll then add the speech recognition and text-to-speech functionality and set the game so you play with your own computer.

As usual, all scripts in this chapter are available at the book’s resources page at https://www.nostarch.com/make-python-talk/. Before you begin, set up the folder /mpt/ch10/ for this chapter.

Game Rules

Tic-tac-toe is probably one of the most well-known games in the world, but just to be sure, I’ll go over the rules before we create our game board. In tic-tac-toe, two players take turns marking a cell with an X or O in a three-by-three grid. The first player to connect three Xs or Os in a row horizontally, vertically, or diagonally wins. If no one connects three before all the cells are full, the game is tied. Instead of X and O, we’ll use blue and white dots as game pieces.

Draw the Game Board

We’ll draw a three-by-three grid on the screen and assign a number to each cell so we can tell the script where to place each game piece. Open your Spyder editor, copy the code in Listing 10-1, and save the script as ttt_board.py in your chapter folder.

import turtle as t

# Set up the screen
t.setup(600,600,10,70)
t.tracer(False)
t.bgcolor("red")
t.hideturtle()
t.title("Tic-Tac-Toe in Turtle Graphics")
# Draw horizontal lines and vertical lines to form grid
t.pensize(5)
1 for i in (-100,100):
    t.up()
    t.goto(i,-300)
    t.down()
    t.goto(i,300)
    t.up()
    t.goto(-300,i)
    t.down()
    t.goto(300,i)
    t.up()
# Create a dictionary to map cell numbers to cell center coordinates
2 cellcenter = {'1':(-200,-200), '2':(0,-200), '3':(200,-200),
            '4':(-200,0), '5':(0,0), '6':(200,0),
            '7':(-200,200), '8':(0,200), '9':(200,200)} 
# Go to the center of each cell, write down the cell number
3 for cell, center in list(cellcenter.items()):
    t.goto(center)
    t.write(cell,font = ('Arial',20,'normal'))
t.done()
try:
    t.bye()
except t.Terminator:
    print('exit turtle')

Listing 10-1: Drawing the tic-tac-toe game board

We import all functions in the turtle module and set the screen to 600 by 600 pixels. Because we have a three-by-three grid, each cell is 200 by 200 pixels. We set the background color to red and set the title as Tic-Tac-Toe in Turtle Graphics.

With the command for i in (-100, 100), we iterate the variable i through the range –100 to 100 1. As a result, the for loop produces two horizontal lines and two vertical lines. The two horizontal lines are between points (–300, –100) and (300, –100) and points (–300, 100) and (300, 100). The two vertical lines are between points (–100, –300) and (–100, 300) and points (100, –300) and (100, 300). These lines evenly divide the screen into nine cells.

We then create a dictionary cellcenter to map each cell number to the x- and y-coordinates of the center of the corresponding cell 2. For example, the lower-left cell is cell number 1, and the coordinates of its center are (x = –200, y = –200). We do this for all nine cells in the dictionary, using the cell number as the key and the coordinates as the value.

At 3, we use the for loop to iterate through nine pairs of values to write the cell number at the cell’s center. The command list(cellcenter.items()) produces a list of the nine key-and-value pairs from cellcenter, which should look like this:

[('1', (-200, -200)), ('2', (0, -200)), ('3', (200, -200)), ('4', (-200, 0)),
('5', (0, 0)), ('6', (200, 0)), ('7', (-200, 200)), ('8', (0, 200)), ('9', 
(200, 200))]

At each iteration of the for loop, the turtle goes to the center of the cell and writes the cell number there. Run the script and you should see a screen similar to Figure 10-1.

f10001

Figure 10-1: The board for tic-tac-toe

Create the Game Pieces

Now we’ll add code to place game pieces in the cells. You’ll first learn how mouse clicks work in the turtle module and then use them to place the pieces.

How Mouse Clicks Work in turtle

When you left-click on the turtle screen, the x- and y-coordinates of the point you clicked are displayed onscreen. Listing 10-2, mouse_click.py, handles a simple mouse click. This is just for example purposes; we won’t use this code in the final script but will use the same principles.

import turtle as t

# Set up the screen
t.setup(620,620,360,100)
t.title("How Mouse-Clicks Work in Turtle Graphics")
# Define get_xy() to print the coordinates of the point you click
1 def get_xy(x,y):
    print(f'(x, y) is ({x}, {y})')
# Hide the turtle so that you don't see the arrowhead
t.hideturtle()
# Bind the mouse click to the get_xy() function
2 t.onscreenclick(get_xy)
3 t.listen()    
t.done()
try:
    t.bye()
except t.Terminator:
    print('exit turtle') 

Listing 10-2: How mouse clicks work in the turtle module

As usual, we import the turtle module and set up the screen. At 1, we define the function get_xy(), which prints out the x- and y-coordinates of your click. We also hide the turtle so you don’t see the cursor moving around the screen. At 2, we bind the onscreen mouse click to the get_xy() function by using the turtle function onscreenclick(), which returns the x- and y-coordinates of the click. As a result, onscreenclick(get_xy) supplies the x- and y-coordinates of your mouse click to get_xy() as its two inputs. At 3, we use listen() to detect events like mouse clicks and keyboard presses.

Run mouse_click.py, randomly click the screen several times, and you should see something like this:

(x, y) is (-46.0, 109.0)
(x, y) is (14.0, -9.0)
(x, y) is (-185.0, -19.0)
(x, y) is (-95.0, 109.0)
(x, y) is (13.0, -81.0)

For each of my five clicks, onscreenclick() captured the x- and y-coordinates of the point and provided the two values to get_xy(), which printed out the corresponding x- and y-values.

Convert Mouse Clicks to Cell Numbers

Next, we’ll combine the board creation and click detection scripts so that when you click a cell, the script prints out the cell number. In Figure 10-2, I’ve marked the row and column numbers on the game board along with the x- and y-coordinates of the gridlines.

Open ttt_board.py, add the code in Listing 10-3 at the bottom (above t.done()) and save the new script as cell_number.py in your chapter folder. This script is just an example; we won’t use it in the final code but will use something similar.

--snip--
for cell, center in list(cellcenter.items()):
    t.goto(center)
    t.write(cell,font = ('Arial',20,'normal'))
# Define a function cell_number() to print out the cell number
1 def cell_number(x,y):
    if -300<x<300 and -300<y<300:
        # Calculate the column number based on x value
      2 col = int((x+500)//200)
        print('column number is ', col)
        # Calculate the row number based on y value
        row = int((y+500)//200)
        print('row number is ', row)
        # Calculate the cell number based on col and row
      3 cellnumber = col+(row-1)*3
        print('cell number is ', cellnumber)
    else:
        print('you have clicked outside the game board')
# Hide turtle so that you don't see the arrowhead
t.hideturtle()
# bind the mouse click to the cell_number() function
  onscreenclick(cell_number)
t.listen()
--snip--

Listing 10-3: Converting mouse clicks to cell numbers

f10002

Figure 10-2: Mark the row and column numbers on the game board.

At 1, we define cell_number(), which will convert the x- and y-coordinates of the mouse click to the cell number. Inside the function, we restrict the x- and y-coordinates of the point you click to the range of the board. If you click outside the range, the script will print you have clicked outside the game board.

At 2, we convert the x-coordinate of the click to the column number. Points in column 1 have x-coordinates between –300 and –100, and points in column 2 have x-coordinates between –100 and 100, so we use the formula col = int((x+500)//200) to get the full range of pixel coordinates in the column so we can convert the x-coordinate to the column number. We use the same method to convert the y-coordinate to the row number.

We then calculate the cell number by using the formula cellnumber = col+(row-1)*3 because the cell numbers increase from left to right and then from bottom to top 3. Finally, we bind the onscreen click to cell_number().

Run cell_number.py. Here’s the output from one exchange with the script:

column number is  3
row number is  2
cell number is  6
column number is  1
row number is  3
cell number is  7
column number is  2
row number is  1
cell number is  2

Each time you click a cell, the script prints out the column number, row number, and cell number.

Place Game Pieces

Next, we’ll place the game pieces on the board. When you first click any of the nine cells, a blue piece will appear at the center of the cell. When you click again, the piece will be white, then blue, and so on.

Open ttt_board.py, add the code in Listing 10-4, and save the new script as mark_cell.py in your chapter folder. Make sure you don’t add this code snippet to cell_number.py!

--snip--
for cell, center in list(cellcenter.items()):
    t.goto(center)
    t.write(cell,font = ('Arial',20,'normal'))
# The blue player moves first
turn = "blue"
# Define a function mark_cell() to place a dot in the cell
1 def mark_cell(x,y):
    # Make the variable turn a global variable
  2 global turn
    # Calculate the cell number based on x and y values
    if -300<x<300 and -300<y<300:
        col = int((x+500)//200)
        row = int((y+500)//200)
        # The cell number is a string variable
      3 cellnumber = str(col + (row - 1)*3)
    else:
        print('you have clicked outside the game board')

    # Go to the corresponding cell and place a dot of the player's color
    t.up()
  4 t.goto(cellcenter[cellnumber])
    t.dot(180,turn)
    t.update()
    # give the turn to the other player
    if turn == "blue":
        turn = "white"
    else:
        turn = "blue"

# Hide the turtle so that you don't see the arrowhead
t.hideturtle()
# Bind the mouse click to the mark_cell() function
t.onscreenclick(Mark_cell)
t.listen()
--snip--

Listing 10-4: Placing game pieces on the board

We draw the board and then define the variable turn that will keep track of whose turn it is. We first assign the value blue to the variable so that the blue player moves first.

At 1, we define mark_cell(), which places a piece in the cell you click. At 2, we declare the global variable turn. Python provides the global keyword, which allows turn to be used both inside and outside mark_cell(). Without making the variable global, you’d get the error message UnboundLocalError: local variable 'turn' referenced before assignment each time you clicked the board.

We then convert the x- and y-coordinates of the click to the cell number on the game board 3. Within the same line, we also convert the cell number from an integer to a string to match the variable type used in the dictionary cellcenter.

At 4, we get the coordinates for the center of the clicked cell from cellcenter and tell the turtle to go there. The turtle places a dot 180 pixels wide and the color of the value stored in turn. After that, the turn is over, and we assign the turn to the other player. Finally, we bind mark_cell() to the mouse-click event.

Run the script and you’ll be able to click the board and mark the cell. The color of the dot will alternate between blue and white, as in Figure 10-3.

f10003

Figure 10-3: Mark cells on the tic-tac-toe board.

The script is now a playable game! However, we need to implement three new rules to make it follow the rules of tic-tac-toe:

  • If a cell is already occupied, you cannot mark it again.
  • If a player marks three cells in a straight line—either horizontally, vertically, or diagonally—the player wins, and the game should stop.
  • If all nine cells are occupied, the game should stop, and a tie should be called if no player wins.

Determine Valid Moves, Wins, and Ties

Next, we’ll implement those rules, allowing only valid moves and declaring wins (or ties). Download ttt_click.py from the book’s resources and save it in your chapter folder or alter mark_cell.py with the differences highlighted in Listing 10-5.

from tkinter import messagebox
--snip--
# The blue player moves first
turn = "blue"
# Count how many rounds played
rounds = 1 1
# Create a list of valid moves
validinputs = list(cellcenter.keys())
# Create a dictionary of moves made by each player
occupied = {"blue":[],"white":[]}
# Determine if a player has won the game
def win_game(): 2
    win = False
    if '1' in occupied[turn] and '2' in occupied[turn] and '3' in occupied[turn]:
        win = True
    if '4' in occupied[turn] and '5' in occupied[turn] and '6' in occupied[turn]:
        win = True
    if '7' in occupied[turn] and '8' in occupied[turn] and '9' in occupied[turn]:
        win = True
    if '1' in occupied[turn] and '4' in occupied[turn] and '7' in occupied[turn]:
        win = True
    if '2' in occupied[turn] and '5' in occupied[turn] and '8' in occupied[turn]:
        win = True
    if '3' in occupied[turn] and '6' in occupied[turn] and '9' in occupied[turn]:
        win = True
    if '1' in occupied[turn] and '5' in occupied[turn] and '9' in occupied[turn]:
        win = True
    if '3' in occupied[turn] and '5' in occupied[turn] and '7' in occupied[turn]:
        win = True
    return win
# Define a function mark_cell() to place a dot in the cell
def mark_cell(x,y):
    # Declare global variables
    global turn, rounds, validinputs 3
    # Calculate the cell number based on x and y values
    if -300<x<300 and -300<y<300:
        col = int((x+500)//200)
        row = int((y+500)//200)
        # The cell number is a string variable
        cellnumber = str(col + (row - 1)*3)
    else:
        print('you have clicked outside the game board')
    # Check if the move is a valid one
    if cellnumber in validinputs: 4
        # Go to the corresponding cell and place a dot of the player's color
        t.up()
        t.goto(cellcenter[cellnumber])
        t.dot(180,turn)
        t.update()
        # Add the move to the occupied list for the player
        occupied[turn].append(cellnumber) 5
        # Disallow the move in future rounds
        validinputs.remove(cellnumber)
        # Check if the player has won the game
        if win_game() == True: 6
            # If a player wins, invalid all moves, end the game
            validinputs = []
            messagebox.showinfo("End Game",f"Congrats player {turn}, you won!")
        # If all cells are occupied and no winner, it's a tie
        elif rounds == 9: 7
            messagebox.showinfo("Tie Game","Game over, it's a tie!")
        # Counting rounds
        rounds += 1
        # Give the turn to the other player
        if turn == "blue":
            turn = "white"
        else:
            turn = "blue"
    # If the move is not a valid move, remind the player 
    else:
        messagebox.showerror("Error","Sorry, that's an invalid move!")
# Bind the mouse click to the mark_cell() function
t.onscreenclick(mark_cell)
--snip--

Listing 10-5: Allow only valid moves and declare wins and ties.

Our first change is to import the messagebox module from the tkinter package; this module displays a message box for a win, tie, or invalid move.

Starting at 1, we create a variable rounds, a list validinputs, and a dictionary occupied. The variable rounds keeps track of the number of turns taken, which is the number of cells that have been marked. When the number of rounds reaches nine and no player wins (which is often the case in tic-tac-toe), we’ll declare a tie game.

We use validinputs to determine whether a move is valid. If a cell is marked by a player, we’ll remove it from the list of valid moves.

The dictionary occupied keeps track of each player’s moves. At the beginning of the game, the keys blue and white both have an empty list as their value. When a player occupies a cell, the cell number will be added to that player’s list. For example, if the blue player has occupied cells 1, 5, and 9 and the white player has occupied cells 3 and 7, occupied will become {"blue":["1","5","9"],"white":["3","7"]}. We’ll use this later to determine whether a player has won the game.

At 2, we define win_game(), which checks whether a player has won the game. There are eight ways a player can win, which we explicitly check for:

  • Cells 1, 2, and 3 have been occupied by the same player.
  • Cells 4, 5, and 6 have been occupied by the same player.
  • Cells 7, 8, and 9 have been occupied by the same player.
  • Cells 1, 4, and 7 have been occupied by the same player.
  • Cells 2, 5, and 8 have been occupied by the same player.
  • Cells 3, 6, and 9 have been occupied by the same player.
  • Cells 1, 5, and 9 have been occupied by the same player.
  • Cells 3, 5, and 7 have been occupied by the same player.

The function win_game() creates the variable win and assigns False as a default value. The function checks the dictionary occupied for the list of cells occupied by the player who currently has the turn, checking all eight win cases listed earlier. If one of the cases matches, the value win changes to True. When win_game() is called, it returns the value stored in the variable win.

We’ve made significant changes to mark_cell(). At 3, we declare three global variables; all must be declared global because they will be modified inside the function. At 4, we check whether the cell number most recently clicked is in the list validinputs; if it is, a dot is placed in the cell, and the cell number is added to the player’s list of occupied cells 5. The cell is then removed from validinputs so that players can’t mark the same cell in future rounds.

At 6, we call win_game() and see whether the current player has won the game. If yes, we change validinputs to an empty list so no further moves can be made. A message box will pop up to say, Congrats player blue, you won! or Congrats player white, you won!, using showinfo() from the messagebox module (Figure 10-4).

f10004

Figure 10-4: A win for blue!

If the player hasn’t won, the script checks whether the number of rounds has reached nine 7. If yes, the script declares a tie game, displaying Game over, it's a tie! (Figure 10-5).

f10005

Figure 10-5: A tied game

If the game doesn’t end, we increase the number of rounds by one and assign the turn to the other player. During the game, if a player clicks an invalid cell, we’ll display Sorry, that's an invalid move! (Figure 10-6).

f10006

Figure 10-6: An invalid move

Voice-Controlled Version

Now we’re ready to add the voice control and speech functionality. One significant change is that we’ll now make your opponent your computer. We’ll build on the latest ttt_click.py file. After you make a move as the blue player, the computer will randomly select a move as the white player until the game ends.

Download ttt_hs.py from the book’s resources or make the changes shown in Listing 10-6.

import turtle as t
from random import choice
from tkinter import messagebox

# Import functions from the local package
from mptpkg import voice_to_text, print_say
--snip--
    if '3' in occupied[turn] and '5' in occupied[turn] and '7' in occupied[turn]:
        win = True
    return win
# Start an infinite loop to take voice inputs
1 while True:
    # Ask for your move
    print_say(f"Player {turn}, what's your move?")
    # Capture your voice input
    inp = voice_to_text()
    print(f"You said {inp}.")
    inp = inp.replace('number ','')
    inp = inp.replace('one','1')   
    inp = inp.replace('two','2')
    inp = inp.replace('three','3')
    inp = inp.replace('four','4')
    inp = inp.replace('five','5')
    inp = inp.replace('six','6')
    inp = inp.replace('seven','7')
    inp = inp.replace('eight','8')
    inp = inp.replace('nine','9')
    if inp in validinputs:
        # Go to the corresponding cell and place a dot of the player's color
        t.up()
        t.goto(cellcenter[inp])
        t.dot(180,turn)
        t.update()
        # Add the move to the occupied list for the player
        occupied[turn].append(inp)
        # Disallow the move in future rounds
        validinputs.remove(inp)
        # Check if the player has won the game
      2 if win_game() == True:
            # If a player wins, invalid all moves, end the game
            validinputs = []
            print_say(f"Congrats player {turn}, you won!")
            messagebox.showinfo
            ("End Game",f"Congrats player {turn}, you won!")
            break

        # If all cells are occupied and no winner, game is a tie
        elif rounds == 9:
            print_say("Game over, it's a tie!")
            messagebox.showinfo("Tie Game","Game over, it's a tie!")
            break

        # Counting rounds
        rounds += 1
        # Give the turn to the other player
        if turn == "blue":
            turn = "white"
        else:
            turn = "blue"  
        
        # The computer makes a random move
      3 inp = choice(validinputs)
        print_say(f'The computer occupies cell {inp}.')
        t.up()
        t.goto(cellcenter[inp])
        t.dot(180,turn)
        t.update()
        occupied[turn].append(inp)
        validinputs.remove(inp)
        if win_game() == True:
            validinputs = []
            print_say(f"Congrats player {turn}, you won!")
            messagebox.showinfo
            ("End Game",f"Congrats player {turn}, you won!")
            break
        elif rounds == 9:
            print_say("Game over, it's a tie!")
            messagebox.showinfo("Tie Game","Game over, it's a tie!")
            
            break
        rounds += 1
        if turn == "blue":
            turn = "white"
        else:
            turn = "blue"     

    # If the move is not a valid move, remind the player 
    else:
        print_say("Sorry, that's an invalid move!")
t.done()
--snip--

Listing 10-6: Adding speech and voice-control functionality

We import the functions we’ll need: the choice() function from the random module to let the computer randomly select a move and our print_say() and voice_to_text() functions from the custom package mptpkg.

At 1, we start an infinite while loop. At each iteration, the script asks for your move out loud. You speak into the microphone to make your move, and the script captures your voice command, storing the response in the variable inp.

Here we did a little tweaking to make voice_to_text() more responsive to your voice commands. When your voice input is just one word, such as “One” or “Two,” it’s hard for the software to put the word in context and respond. On the other hand, if you say “Number one” or “Number two,” the software can easily pick up your meaning. The script simply replaces the “number” part of the voice command with an empty string so that only the number is left in inp. Sometimes voice_to_text() returns the number in word form such as one or two, instead of in numeric form, such as 1 or 2. We therefore also change all the word forms to numerical forms. This way, you can say “number one” or “one” to the microphone, and inp will always be in the form you want: 1.

If your choice is in validinputs, the script performs the sequence of actions to make the move: place a dot in the corresponding cell, add the cell number to your list of occupied cells, and remove the occupied cell number from the list of valid inputs.

The script then checks if you’ve won or tied the game 2 and responds out loud appropriately.

Once your turn is over, the computer randomly selects a move from validinputs to play against you 3. The script checks whether the computer has won or tied the game. If your voice command is not a valid move, the script speaks an alert.

Here’s one interaction with the game:

Player blue, what's your move?
You said 7.
The computer occupies cell 3.
Player blue, what's your move?
You said 8.
The computer occupies cell 1.
Player blue, what's your move?
You said 9.
Congrats player blue, you won!

I’ve managed to win in just three moves!

Summary

In this chapter, you learned to build a voice-controlled graphical tic-tac-toe game that talks in a human voice. Along the way, you learned a few new skills.

You learned how mouse clicks work in the turtle module. With that knowledge, we marked cells on the game board with mouse clicks.

You learned how to determine whether a player has won tic-tac-toe based on the explicit game rules. This is at the heart of game creation. You listed all cases when a player can win the game, then added code to check all cases and see whether there is a winner.

You also added the speech recognition and text-to-speech features to a game, making a few tweaks to make sure the script can understand your input. By combining these skills, you’ll be able to create your own voice-controlled games.

End-of-Chapter Exercises

  1. Modify ttt_board.py so that the cell number appears in 15-point font at the lower-left corner of each cell (80 pixels from the center of the cell, both horizontally and vertically).
  2. Modify mouse_click.py so that each time you click the screen, the script prints out the additional message x + y is, followed by the actual value of the x- and y-coordinates of the clicked point.
  3. Modify cell_number.py so that each time you click the screen, the script prints you clicked the point (x, y) before printing the column, row, and cell numbers, where x and y are the actual coordinates. For example, if you click the point (x = –100, y = 50), the message should say you clicked the point (-100, 50).
  4. Modify mark_cell.py so that the white player moves first.
  5. Modify ttt_click.py so that a player wins only by marking three cells in a row horizontally or vertically, but not diagonally.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.152.173