Learn Google Cloud Functions by building a simple Tic Tac Toe game in Python

Hello!

Want to learn about Google Cloud? I hope this quick project can be and enjoyable way for you to learn a new skill or two. A goal was to have this information be usable to people of various skill levels and experiences.

This tutorial will be divided into the following parts:

  1. Setup
  2. Google Cloud Functions ‘Hello World!’
  3. Tic Tac Toe bases cases
  4. Make a move
  5. Code Cleanup
  6. Retrospective

Setup

First thing is log into Google Cloud. — https://console.cloud.google.com. Create an account if you don’t have one.

Let’s create a new project. — https://console.cloud.google.com/projectcreate

  • Choose a name.
  • You can leave the organization blank.

Once you have created the project, you should be at the default dashboard.

Next we will need to install google cloud sdk on your local machine. This will be used to test connecting to the Google Cloud Function. It can be downloaded from here: https://cloud.google.com/sdk

After installing, make sure to configure with ‘gcloud init’.

Lastly, make sure you can use the CURL program. Most modern operating systems come with this program by default.

NOTE: There are multiple ways to test your function. I will give commands to run the CURL program. After writing this post, I found that when selecting your function in the interface, there is a ‘testing’ tab that can be used easily inside your browser. That may be easier for new users.

Google Cloud Functions ‘Hello World!’

Lets create a function.

Select the navigation panel from the top-left, and access ‘Cloud Functions’

Select ‘create function’

Now you should be on the page to create a new Google Cloud Function. Let’s change the name and the memory allocated.

Our function is simple, so it should not need much memory. Lowering memory allocation will lower the costs for running our function.

Next we will need to change the runtime to python.

Once the Runtime is changed to python, we should have the basic template we need. For simplicity sake, we will be using the inline editor to write our function.

That should be enough for now. Let’s see where we are at.

Try creating the function.

Once it is created, let’s try to access it with CURL (replace the url with your functions url):

curl https://us-central1-tic-tac-toe-275323.cloudfunctions.net/tictactoe

That should have been unsuccessful. That is because it requires credentials to access. It is important to require credentials so that you don’t accrue GCP costs from other users.

Let’s add the credentials using the gcloud sdk:

curl https://us-central1-tic-tac-toe-275323.cloudfunctions.net/tictactoe -H “Authorization: bearer $(gcloud auth print-identity-token)”

You just successfully ran your serverless function!

Tic Tac Toe base cases

We will be representing a tic tac toe board as a 2D array.

Like solving most problems, starting with the base cases can be a good methodology. I would like to validate the following:

  1. The tic tac toe board was included in the request
  2. The tic tac toe board has a length of 3
  3. Each element of the board is an array of length 3
  4. Each element in the tic tac toe board is one of the following: [‘X’,’0',’’]
  5. A player has not already won
  6. There is an available move

Validate board was included in request

Let’s add the following code to our function:

request_json = request.get_json()try:    board = request_json[‘board’]except:    return ‘Did not include board!!!’

Test it out. The previous CURL example should prompt that we did not return a board. The following example will show how we can send the board variable to the Google Cloud Function.

curl https://us-central1-tic-tac-toe-275323.ctactoe -H “Authorization: bearer $(gcloud auth print-identity-token)” -H “Content-Type:application/json” -d ‘{“board”:”1"}’

Now we have our first validation. Pretty easy, right? :)

The tic tac toe board has is an array of length of 3

Add the following code to your Google Cloud Function.

if not isinstance(board, list):    return "Invalid board. Must be a list"if len(board) != 3:    return "Invalid board. Outer array is not of length 3"

Test it. Again, the previous CURL we used did not satisfy the new constraint. Below is an example CURL that would satisfy the new constraint.

curl https://us-central1-tic-tac-tactoe -H "Authorization: bearer $(gcloud auth print-identity-token)" -H "Content-Type:application/json" -d '{"board":[1,2,3]}'

Each element of the board is an array of length 3

Add the following code to your Google Cloud Function.

for i, row in enumerate(board):
if not isinstance(row, list):
return f"Invalid board. Row {i} is not a list"
if len(row) != 3:
return f"Invalid board. Row {i} is not length 3"

The following CURL request will satisfy the new constraint:

curl https://us-central1-tic-tac-toe-275323.cloudfunctions.net/tictactoe -H "Authorization: bearer $(gcloud auth print-identity-token)" -H "Content-Type:application/json" -d '{"board":[[1,2,3],[1,2,3],[1,2,2]]}'

Each element in the tic tac toe board is one of the following: [‘X’,’0',’’]

We will need to add the following line:

for j, elem in enumerate(row):
if elem not in ['X','O','']:
return f'Invalid board. Elem [{i}][{j}] not valid.'

Your complete function should now look something like this:

...request_json = request.get_json()try:    board = request_json['board']except:    return 'Did not include board!!!'if not isinstance(board, list):    return "Invalid board. Must be a list"if len(board) != 3:    return "Invalid board. Outer array is not of length 3"for i, row in enumerate(board):    if not isinstance(row, list):        return f"Invalid board. Row {i} is not a list"    if len(row) != 3:        return f"Invalid board. Row {i} is not length 3"    for j, elem in enumerate(row):        if elem not in ['X','O','']:            return f'Invalid board. Elem [{i}][{j}] not valid.'return f'testing'

The below CURL satisfies the new constraint:

curl https://us-central1-tic-tactoe -H "Authorization: bearer $(gcloud auth print-identity-token)" -H "Content-Type:application/json" -d '{"board":[["","",""],["X","",""],["","",""]]}'

A player has not already won

Next, we will want to return to the client when a user has already won. Below is an implementation.

for i in range(len(board)):
# Check rows
if board[i].count('X') == len(board):
return 'You already won!'
if board[i].count('O') == len(board):
return 'I already won!'

# Check columns
if len( {board[0][i], board[1][i], board[2][i]} ) == 1:
if board[0][i] == 'X':
return 'You already won!'
if board[0][i] == 'O':
return 'I already won!'

# Check diagonals
if 1 in [
len( {board[0][0], board[1][1], board[2][2]} ),
len( {board[2][0], board[1][1], board[0][2]} )]: if board[1][1] == 'X':
return 'You already won!'
if board[1][1] == 'O':
return 'I already won!'

There is an available move

The last check we will do is find an available move. We will use a boolean variable and add a check inside one of our previous loops.

move_available = False...
...
for j, elem in enumerate(row):
if elem == '': move_available = True......
if move_available == False:
return "Invalid board. Cat's game board"

Make a move

All we have left is to return where the server decides to go. For now we will have it place it’s move with simply heuristic:

We will change the boolean we created a moment ago to store the position.

move = None...
...
for j, elem in enumerate(row):
if elem == '': move = [i,j]
...
...
if not move:
return "Invalid board. Cat's game board"
...
...
board[move[0]][move[1]] = 'O'
return f'{board}'

With that, we should have a ‘functional’ tic tac toe function. One thing is that the servers response will be very predictable. It is easy to add randomization to a program to fix this. Start by importing the random module at the top of the file. We will and some randomization to the servers move choice.

import random
...
...
for j, elem in enumerate(row):
if elem == '': if not move or random.randrange(100) < 50: move = [i,j]...
...

This is the entire code for our serverless tic tac toe game.

import randomdef tic_tac_toe(request):    request_json = request.get_json()    try:        board = request_json['board']    except:        return 'Did not include board!!!'    if not isinstance(board, list):        return "Invalid board. Must be a list"    if len(board) != 3:        return "Invalid board. Outer array is not of length 3"    move = None    for i, row in enumerate(board):        if not isinstance(row, list):            return f"Invalid board. Row {i} is not a list"        if len(row) != 3:            return f"Invalid board. Row {i} is not length 3"        for j, elem in enumerate(row):            if elem == '':                if not move or random.randrange(100) < 50:                    move = [i,j]            if elem not in ['X','O','']:                return f'Invalid board. Elem [{i}][{j}] not valid.'    if not move:        return "Invalid board. Cat's game board"    for i in range(len(board)):        if board[i].count('X') == len(board):            return 'You already won!'        if board[i].count('O') == len(board):            return 'I already won!'        if len( {board[0][i], board[1][i], board[2][i]} ) == 1:            if board[0][i] == 'X': 
return 'You already won!'
if board[0][i] == 'O':
return 'I already won!'
# Check diagonals
if 1 in [
len( {board[0][0], board[1][1], board[2][2]} ),
len( {board[2][0], board[1][1], board[0][2]} )]:
if board[1][1] == 'X':
return 'You already won!'
if board[1][1] == 'O':
return 'I already won!'
board[move[0]][move[1]] = 'O' return f'{board}'

Code Cleanup

Do you see any shortcomings in our current code?

The most obvious to me is the fact that we are using two identical for loops. Lets change that.

Next, check out this part of the code:

...
if elem == '':
if not move or random.randrange(100) < 25:
move = [i,j]
...

How about this?😺

...
if elem == '' and (not move or random.randrange(100) < 25):
move = [i,j]
...

Restrospective

I had fun working on this project. I think Google Cloud Functions and other serverless computing services are powerful :)!

While working on it there were times when I would have liked to have used a single list instead of a 2D list. I think this could have resulted in cleaner code. That, or have access to numpy for advanced operations on multidimensional arrays.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store