Play Chess with a Friend on WhatsApp using Python and Twilio

June 03, 2020
Written by
Reviewed by
Diane Phan
Twilion

Play Chess with a Friend on WhatsApp using Python and Twilio

While most of the world is under some form of stay-at-home orders to prevent the spread of COVID-19, we are constantly looking for new ways to entertain ourselves and remain connected with family and friends.

When I was little I used to play chess a lot, so now that I have additional time on my hands I decided to start playing again. The thing is, nobody in my immediate family plays chess, so I had this idea of using WhatsApp to play a game of chess against a remote friend.

Chess game demo

By the end of this tutorial you will know how to use the Twilio API for WhatsApp to implement a turn-based game that requires sending messages and images between multiple participants. You will also learn about how to manage a game of chess in Python, but even if chess isn’t your thing, you will be able to replace the chess portions with your favorite turn-based game!

Tutorial requirements

To build this project you need the following items:

  • Python 3.6 or newer. If your operating system does not provide a Python interpreter, you can go to python.org to download an installer.
  • A free or paid Twilio account. If you are new to Twilio get your free account now! This link will give you $10 credit when you upgrade.
  • Two WhatsApp or SMS enabled smartphones.
  • ngrok. We will use this handy utility to connect the development version of our Python application running on your system to a public URL that Twilio can connect to. This is necessary for the development version of the application because your computer is likely behind a router or firewall, so it isn’t directly reachable on the Internet. If you don’t have ngrok installed, you can download a copy for Windows, MacOS or Linux.

It is highly recommended that you create a free Ngrok account and install your Ngrok account's authtoken on your computer to avoid hitting limitations in this service. See this blog post for details.

Ready to play? Let’s go!

Configuration

Let’s start by creating a directory where our project will live:

$ mkdir twilio-chess
$ cd twilio-chess

Following best practices, we are now going to create a virtual environment where we will install our Python dependencies.

If you are using a Unix or MacOS system, open a terminal and enter the following commands to do the tasks described above:

$ python -m venv venv
$ source venv/bin/activate
(venv) $ pip install flask python-dotenv python-chess cairosvg twilio

For those of you following the tutorial on Windows, enter the following commands in a command prompt window:

$ python -m venv venv
$ venv\Scripts\activate
(venv) $ pip install flask python-dotenv python-chess cairosvg twilio

The Python packages that we have installed are:

  • The Flask framework, to create the web application that will receive message notifications from Twilio
  • The python-dotenv package, to manage our environment variables
  • The python-chess package, to maintain the state of the chess board
  • The cairosvg package, to render chess board images that we can send through WhatsApp and SMS
  • The python-twilio package, to send messages through the Twilio service

Installing the Cairo Graphics Library

The cairosvg package installed above requires the Cairo Graphics Library installed in your system. Use the appropriate instructions for your operating system to install this library:

  • For Windows 64-bit: download libcairo-x64.zip and extract its contents in the project directory or any other directory that is in the system path.
  • For Windows 32-bit: download libcairo-x86.zip and extract its contents in the project directory or any other directory that is in the system path.
  • For Mac OS: run brew install cairo (this requires the homebrew package manager).
  • For Ubuntu Linux: run sudo apt-get install libcairo2.
  • For other Linux distributions: consult the documentation for your package manager to find out what is the package you need to install for libcairo.

Configure the Twilio WhatsApp Sandbox

Twilio provides a WhatsApp sandbox where you can easily develop and test your application. Once your application is complete you can request production access for your Twilio phone number, which requires approval by WhatsApp.

Let’s connect your smartphone to the sandbox. From your Twilio Console, select Programmable SMS and then click on WhatsApp. The WhatsApp sandbox page will show you the sandbox number assigned to your account, and a code to join.

Configure Twilio sandbox for WhatsApp

To enable the WhatsApp sandbox for your smartphone send a WhatsApp message with the given code to the number assigned to your account. The code is going to begin with the word join, followed by a randomly generated two-word phrase. Shortly after you send the message you should receive a reply from Twilio indicating that your mobile number is connected to the sandbox and can start sending and receiving messages.

This step needs to be repeated for any additional phones you’d like to have connected to your sandbox, so you will need to ask your chess playing friend to do this as well.

 

Receiving and sending messages with Twilio

The two participants will enter their moves as text messages. When the application receives a message from the first player, it will confirm the receipt by replying to the player. Then it will also send a separate message to the second player, including the updated image of the board. Once the second player sends their move the cycle repeats with the players reversed.

Receiving a message

The Twilio API for WhatsApp uses a webhook to notify an application when there is an incoming message. Our chess application is going to use the Flask framework to define an endpoint that Twilio can invoke when a player sends in their move.

To write your first webhook, enter the following code in a app.py file:

from dotenv import load_dotenv
from flask import Flask, request

app = Flask(__name__)


@app.route('/webhook', methods=['POST'])
def webhook():
    player = request.form.get('From')
    message = request.form.get('Body').lower()

    # TODO: send a response

The @app.route decorator from Flask associates the webhook() function with the /webhook URL. Later we will tell Twilio to notify us of incoming messages by sending a request to this URL.

This example also shows how to obtain the sender of the message and the message text. Both of these values come as form variables, which Flask makes accessible to us in the request.form dictionary. The From form variable is the phone number of the sender, which in the case of a WhatsApp message is given in the format whatsapp:<phone number>. For example, if we received a WhatsApp message from the United States number 234-567-8900, we would receive it as whatsapp:+12345678900. The text of the message comes in the Body variable, and since we’ll need to parse chess moves from this text we normalize it to lowercase.

Replying to a message

Once we receive a move from a player we would like to acknowledge it by responding to the player. The webhook can issue a response to the sender of a message by providing a TwiML payload in the response of the webhook. TwiML is short for Twilio Markup Language, an XML-based syntax that the webhook can use to provide instructions to Twilio regarding how to handle the message.

The Twilio Helper Library for Python allows us to generate TwiML responses using classes and objects. The respond() function shown below generates a response that sends a text message back to the sender:

from twilio.twiml.messaging_response import MessagingResponse


def respond(message):
    response = MessagingResponse()
    response.message(message)
    return str(response)

Add the new import at the top of the app.py file. The new respond() function can be above or below webhook(), it doesn't really matter.

If you want to see how TwiML looks, start a Python shell and enter the following code to generate an example response:

>>> from app import respond
>>> respond('this is my response')
'<?xml version="1.0" encoding="UTF-8"?><Response><Message>this is my response</Message></Response>'

Let’s incorporate the use of our new respond() function into the webhook function:

@app.route('/webhook', methods=['POST'])
def webhook():
    player = request.form.get('From')
    message = request.form.get('Body').lower()
    return respond(f'You are: {player}.\nYou said: {message}')

Setting up a webhook with Twilio

We now have a complete webhook function that receives a message and responds to it, so let’s run it to make sure it is working properly.

Add a .flaskenv file (note the leading dot) to your project with the following contents:

FLASK_APP=app.py
FLASK_ENV=development

This tells the Flask framework the location of our application, and also configures Flask to run in debug mode, which adds convenient features such as the automatic reloading of the server when changes are made to the code. This is useful because you can start the server and let it run while you code your application, knowing that every time you save a source file the server will reload and incorporate the changes.

Start your Flask application by running the following command:

(venv) $ flask run

The service is now running as a private service on port 5000 inside your computer and will sit there waiting for incoming connections. To make this service reachable to Twilio over the Internet we need to use ngrok.

Leave the Flask terminal running and open a second terminal. Start ngrok with the following command:

$ ngrok http 5000

This command allocates a temporary public domain that redirects HTTP requests to our local port 5000. On a Unix or Mac OS computer you may need to use ./ngrok http 5000 if you have the ngrok executable in your current directory. The output of ngrok should be something like this:

ngrok screenshot

Note the lines beginning with “Forwarding”. These show the public URL that ngrok uses to redirect requests into our service. What we need to do now is tell Twilio to use this URL to send incoming message notifications.

Go back to the Twilio Console, click on Programmable SMS, then on WhatsApp, and finally on Sandbox. Copy the https:// URL from the ngrok output and then paste it on the “When a message comes in” field. Since our endpoint was defined under a /webhook URL, append that at the end of the root ngrok URL. In my case, the complete webhook URL is https://bbf1b72b.ngrok.io/webhook.

Webhook for Twilio WhatsApp sandbox

Make sure the request method is set to HTTP Post. Don’t forget to click the “Save” button at the bottom of the page to record these changes.

Now go to your smartphone and send a WhatsApp message to the WhatsApp sandbox number that you connected to earlier.

whatsapp demo

Sending a message

We now have seen how to receive messages and reply to them. The last remaining messaging feature we will need to use in our chess game is the ability to initiate a conversation with a player, as opposed to responding to a message initiated by them. We will use this to let a player know it is their turn to move after the other player sent their move.

Initiating a message to a user is done by contacting the Twilio APIs. The Twilio Helper Library provides a Client() object that provides Python wrappers for API calls.

The client object needs to authenticate against the Twilio service. The authentication credentials are imported directly from environment variables, so we are going to set those variables in the .env file of our project. Create a file named .env (note the leading dot) and enter the following contents in it:

TWILIO_ACCOUNT_SID="<your account SID>"
TWILIO_AUTH_TOKEN="<your auth token>"
TWILIO_NUMBER="whatsapp:+14155238886"  # Twilio sandbox number
MY_NUMBER="whatsapp:+12345678900"  # your phone number
FRIEND_NUMBER="whatsapp:+19876543200"  # your friend's phone number

For the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN variables, you can obtain the values that apply to your Twilio account from the Twilio Console:

Twilio account SID and auth token

For the TWILIO_NUMBER variable, use the number that was assigned to your WhatsApp sandbox. The format for this number is a whatsapp: prefix, followed by the E.164 representation of the phone number. In my case, the number is whatsapp:+14155238886. Yours might be different, so make sure you use the right number for your account.

The MY_NUMBER and FRIEND_NUMBER are the WhatsApp numbers for yourself and your chess-playing friend. These numbers are given in the same format as the previous one.

Now that we have the configuration in place, let’s send a message to ourselves from the Python shell:

>>> import os
>>> from dotenv import load_dotenv
>>> from twilio.rest import Client
>>> load_dotenv()
True
>>> client = Client()
>>> client.messages.create(from_=os.environ.get('TWILIO_NUMBER'), to=os.environ.get('MY_NUMBER'), body='hi there!')

With this short Python snippet we import the os package to read environment variables, the load_dotenv() function to import the variables we entered in the .env file, and the Client() class from Twilio to send a request to the Twilio API. The client.messages.create() function takes the sender’s number (our WhatsApp sandbox number), the recipient (your own WhatsApp number), and a message.

Check your WhatsApp to confirm that you have received the message. To avoid unsolicited messages, remember that the recipient phone number must have connected to the WhatsApp sandbox before messages are delivered. A WhatsApp session lasts 24 hours, so after that period has passed you will need to reconnect by sending the join message again.

As part of our chess game we will need to send an image of the chess board. To send an image along with your message, you can add the media_url argument to the client.messages.create() call, passing a publicly-accessible URL for the image. We will see later how to generate chess board images to use in our game.

Running a chess game with python-chess

In this section we’ll learn how to work with the python-chess package to manage a game of chess.

To create a new game with all the pieces set to the initial positions, we just create a chess.Board() object:

>>> import chess
>>> board = chess.Board()

If you want to see the state of the game, we can generate the standard FEN (Forsyth-Edwards Notation) notation for the starting position of the board. The FEN notation encodes the position of all the pieces in the board, which player is to move next, and other aspects of the game such as castling and en-passant states.

>>> board.fen()
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'

To make a move, we create a chess.Move object, and then push it into the board object:

>>> board.push(chess.Move.from_uci('e2e4'))
>>> board.fen()
'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1'

In this example I have entered the move e2e4 using a variant of the long algebraic notation, used by the Universal Chess Interface (UCI). This format is simple to learn, as it uses the coordinates of the start and end squares for the moved piece, with the square coordinates using a letter from a to h for the column, and a number from 1 to 8 for the row.

Almost all moves can be represented with these four characters. For castling moves, only the king’s movement is indicated. For a promotion, a fifth character is added with the promoted piece (q for a queen, r for a rook, and so on).

The most interesting aspect of our game is the nice chess board images that we are going to send to the players when it is their turn to move. The chess package comes with support for rendering a board object in the SVG (Scalable Vector Graphics) format, but this isn’t a format supported by Twilio, so we are going to use the cairosvg package to convert the SVG data to PNG.

Let’s make an image for our board` object from the example above:

>>> import chess.svg
>>> import cairosvg
>>> cairosvg.svg2png(bytestring=chess.svg.board(board), write_to='board.png')

You should now have a board.png file in your current directory with the image of the board.

chess board image

The chess.svg.board() function that renders the board to SVG has additional options. The lastmove option can be used to provide a move object. This highlights the start and end square. The flipped boolean can be set to True to render the board from the black player’s perspective.

Below is another call to render the board with the pawn move from e2 to e4 highlighted and in the correct orientation for the black player:

>>> cairosvg.svg2png(bytestring=chess.svg.board(board, lastmove=chess.Move.from_uci('e2e4'), flipped=True), write_to='board.png')

chess board image oriented for black player

Later you will learn how to make these PNG image files available as URLs that we can use in the media_url argument of the message sending function.

Implementing a chess-playing webhook

Let’s go back to our webhook and start incorporating the messaging and chess functionality we have learned in the last two sections.

We are going to design our application as a “bot”. The players will send messages to the application that will be interpreted as commands that change the application state. This is going to include three possible commands:

  • play will start a game. Any of the two players (configured with the MY_NUMBER and FRIEND_NUMBER) can send this command to initiate a game.
  • resign will end a game. Once again any of the players can send this when it is their turn to move.
  • A valid UCI move, such as e2e4, e7e5, etc. When the application receives a move it applies it to the board, and then notifies the other player that it is their turn to move.

Let’s start a new version of app.py file. The imports and global definitions are shown below:

from io import BytesIO
import os
import random
import chess
import chess.svg
import cairosvg
from dotenv import load_dotenv
from flask import Flask, request, url_for
from twilio.rest import Client
from twilio.twiml.messaging_response import MessagingResponse

load_dotenv()

app = Flask(__name__)
client = Client()
twilio_number = os.environ.get('TWILIO_NUMBER')
players = [
    os.environ['MY_NUMBER'],
    os.environ['FRIEND_NUMBER'],
]
board = None

You should recognize most of the imports. The only ones we haven’t covered before are BytesIO, which we will use to render the PNG board images to memory instead of a file, and random, which we will use to randomly select who are the white and black players. We also have a new import from the Flask framework, the url_for() function, which we will use to generate the URLs for the board images, as you will see later.

We call load_dotenv() before we do anything else to import all the variables we have defined in the .env file. The app variable is our Flask application as before. We define a few more variables: client is our Twilio client instance, twilio_number is the number from which we will be sending messages, imported from the environment variable. The players list contains the two players, also imported from the environment variables. Finally, we’ll store the state of the game in the board variable.

We will use the respond() function that we tested above, so let’s add it to our app.py file:

def respond(message):
    response = MessagingResponse()
    response.message(message)
    return str(response)

Our /webhook endpoint is defined below:

@app.route('/webhook', methods=['POST'])
def webhook():
    player = request.form.get('From')
    if player not in players:
        return respond('You are not a player. Bye!')
    opponent = players[0] if players[1] == player else players[1]
    message = request.form.get('Body').lower()

    if message == 'play':
        return new_game()

    if message == 'resign':
        return resign_game(opponent)

    if board is None:
        return respond('You are not currently playing a game. '
                       'Type "play" to start one.')

    if (board.turn == chess.WHITE and player != players[0]) or \
            (board.turn == chess.BLACK and player != players[1]):
        return respond('Not your turn to move!')

    try:
        move = chess.Move.from_uci(message)
        if move not in board.legal_moves:
            raise ValueError()
    except ValueError:
        return respond('Invalid move, please try again.')

    board.push(move)
    return send_move(opponent, move)

This is longer than our previous version, so let’s go over the logic in detail.

We begin by getting the sender of the message in the player local variable. If the player is not any of the known players stored in the players list, then we send an error response and exit. We are only going to accept messages from the two players.

Then the opponent local variable is set to the other player for convenience, as we can now refer to player and opponent. The message is loaded with the body of the message as before.

Next we look at the message to see if we recognize the play or the resign commands. If the former, we call the new_game() function. For the latter we call resign_game(). If the message is neither, we assume it is a move.

Before we can apply the move we have to validate that we are in the correct state. If board is None that means that a game has not started yet, so we send an error message and exit. If the sender is not the player that moves next, we also send an error message. If our validation checks passed, then we try to create the move object and validate it against the board, catching errors in case the move is invalid or illegal. If we find the move is bad, then we return an error message and exit once again.

We finally apply the move to the board and invoke the send_move() function to notify the players that the game has advanced.

As you can see, we have pushed all the game logic into auxiliary functions. Let’s look at these functions one by one.

Creating a new game

Below is the definition of the new_game() function.

def new_game():
    global board, players
    if board is not None:
        return respond('A game is currently in progress. '
                       'Type "resign" to end it.')
    board = chess.Board()
    random.shuffle(players)
    send_move(players[0])
    return str(MessagingResponse())

The function first validates that a game does not currently exist, and if one does then it exists with an error message. To create a new game we create a new chess.Board() instance, and then randomize the players list. We will assign players[0] to the white pieces and players[1] to the black.

At the end we call the send_move() function, which we have yet to define. This is to notify the white player that it is their turn to move. The response that goes out to the sender of the play message is an empty MessagingResponse object, since in this case there is nothing to say.

Ending the game

To keep this project simple the application does not detect end of game conditions such as checkmate or stalemate (though this could be an interesting addition that you can implement using the python-chess package to detect these states). The only way to end the game is when a player resigns. At this point we have to tell the other player that they won, and then reset the board variable back to None, leaving it ready to start another game if desired.

def resign_game(winner):
    global board, players
    if board is None:
        return respond('You are not currently playing a game. '
                       'Type "play" to start one.')
    board = None

    client.messages.create(from_=twilio_number,
                           to=winner,
                           body='Your opponent resigned. You win!')
    return respond('Okay.')

Notifying players when a move was made

Here is the most important function in this application, which tells the player who made a move that the move was accepted, and also tells their opponent that it is their turn to move.

def send_move(player, last_move=None):
    is_black = player == players[1]
    client.messages.create(from_=twilio_number, to=player,
                           body='It\'s your turn to play!',
                           media_url=url_for(
                               'render_board', fen=board.fen(),
                               last_move=last_move,
                               flip='1' if is_black else None,
                               _external=True))
    return respond('Got it! Now waiting for your opponent to move.')

This is a surprisingly short function, given the amount of complexity it packs. In this function we have to send two messages. The response to the player that made the move is in the last line, and is simple because we can use the respond() helper function.

The message that goes to the opponent uses the client.messages.create() function that we’ve seen before. This message includes the media_url argument, on which we invoke the url_for() function from the Flask framework to generate a URL for our board.

How do these URLs work? What we are going to do is add a second endpoint to our application, with the /board URL that returns board images. The url_for() call generates a URL for this endpoint, which Flask will know as render_board because that is the name of the function we are going to write for it.

The board-rendering endpoint will accept a few arguments. We will pass the FEN notation of the board we want to render in the fen argument, the move we want to highlight in last_move and the flip argument will be set to 1 if we want to render the board for the black player. These arguments are going to be given in the query string of the URL.

The URLs that render boards are going to be long due to the length of the FEN strings. An example URL can be https://bbf1b72b.ngrok.io/board?fen=rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR+b KQkq+-+0+1&last_move=e2e4&flipped=1.

Generating board images

Here is the final piece of our application: the board-rendering endpoint we referenced in the send_move() function above.

@app.route('/board')
def render_board():
    board = chess.Board(request.args.get('fen'))
    flip = request.args.get('flip', False)
    last_move = None
    if 'last_move' in request.args:
        last_move = chess.Move.from_uci(request.args.get('last_move'))
    png = BytesIO()
    cairosvg.svg2png(bytestring=chess.svg.board(
        board, lastmove=last_move, flipped=flip), write_to=png)
    return png.getvalue(), 200, {'Content-Type': 'image/png'}

The request.args dictionary from Flask is used to access the fen, flip and last_move query string arguments. With this information we can create a board object that matches the state of the game that is being requested, and then render it to PNG as we did before. For this function, however, we used a BytesIO memory string in the write_to argument, so that the PNG data is written to memory instead of a file.

The endpoint returns the PNG data directly, also setting the image/png content type in the response, so that Twilio knows we are sending an image.

Running the game

If you need to make sure you have all the code, see the complete and tested code in this gist.

If you aren’t running the Flask server anymore, start it again now:

(venv) $ flask run

If ngrok isn’t running, then start it in a second terminal:

$ ngrok http 5000

Make sure that the WhatsApp endpoint is configured with the forwarding URL reported by ngrok. Each time ngrok restarts the URL changes, so you will need to go back to the Sandbox settings and update the notification webhook. Remember to add /webhook at the end of the ngrok forward URL.

Make sure that both you and your friend have connected to the WhatsApp sandbox in the last 24 hours. Then start WhatsApp on your phone and send play to the sandbox number. The application will select one of the players to play white and send them a board.

Start making moves in the UCI format and have fun playing chess with your friend!

making a chess move on whatsapp

Using SMS instead of WhatsApp

The last topic we are going to cover is how to use SMS instead of WhatsApp. The SMS and WhatsApp APIs are identical, so this application can easily be adapted to run on SMS and MMS.

First, you have to acquire a Twilio phone number with SMS capabilities. This is going to take the place of the WhatsApp sandbox. Use the Buy a Number page to find a number in your country and region that you like.

The webhook for incoming message notifications is configured separately for WhatsApp and SMS. To do it with your new Twilio phone number, select the number in the Phone Numbers page, scroll down to the “Messaging” section and enter the ngrok endpoint URL in the “When a message comes in” field.

SMS webhook configuration

Finally, open your .env file and replace all the WhatsApp numbers with regular numbers, still in E.164 format, but without the whatsapp: prefix.

Now restart the Flask application to have it use the updated environment variables and send messages to the Twilio number to play chess with your friend!

SMS chess play screenshot

Conclusion

I hope this was a fun and interesting tutorial, and that you continue developing the game on your own. In particular, I see a few nice improvements that can be added:

  • Detect when the game ends. The python-chess package can do this.
  • Implement other turn-based games that you like. Random chess instead of standard chess should be very easy!

I’d love to see in which ways you extend this little project!

Miguel Grinberg is a Python Developer for Technical Content at Twilio. Reach out to him at mgrinberg [at] twilio [dot] com if you have a cool Python project you’d like to share on this blog!