How to build a picture guessing game with Twilio

February 05, 2024
Written by
Anthony
Twilion
Reviewed by
Paul Kamp
Twilion

How to build a picture guessing game with Twilio

Twilio is known among developers for having the go-to APIs for delivery notifications, promotional offers, emergency alerts, and account verification – but did you know that Twilio is also used for good old-fashioned fun?

In this blog post, we developers will channel our creativity and artistic flair to build a picture guessing game.

Feel free to build along, or fork the project on GitHub to add your own unique flair. You can play the game by texting “GO” to +1 (844) 905-5079.

Please note that this game was developed in Python, and the corresponding code can be found in the GitHub repository .
Anthony live coding animation

Background

To start the game, text “GO” to +1 (844) 905-5079. You’ll receive an animated GIF of a random drawing and have 3 attempts to guess what it is. Some of the images are easier to guess than others, and that’s because they come from real-world drawings through Quick, Draw!

Quick, Draw! is an online guessing game by Google that challenges real players to doodle objects; the goal being to have a neural network AI recognize the drawings. With over 15 million players contributing more than 50 million drawings, it’s the world’s largest doodling data set . This large volume of drawings gives our players a great deal of variety in their playthroughs.

To interact with the data set, we’re using quickdraw , a Python API for accessing the Quick, Draw! data.

Now on to the tutorial!

Step 0: Tools to install

Step 1: Create a free Twilio account

In order to build this yourself or use other Twilio products , you’ll need a Twilio account. If you haven’t already, sign up for a free Twilio account . The signup process is quick and no credit card is required!

Step 2: Buy a Twilio phone number

If you haven’t done so already, buy a Twilio phone number – a phone number purchased through Twilio – to send messages.

After signing up for an account, log in to the Twilio Console . Then, navigate to the Phone Numbers page. Click Buy a Number to purchase a Twilio number.

With a 10DLC number, A2P 10DLC registration is required for US messaging.
Buy a phone number with Twilio
When you sign up for a free Twilio account , you’re given a free trial balance ($15 in the United States) to experiment with. Twilio pricing uses a pay-as-you-go usage-based model for SMS so you aren’t locked into any contracts.

Step 3: Setup local development environment

After creating a Twilio account and obtaining a phone number, this step sets up a project to accommodate your code.

Open a Terminal window and create an empty project directory called twilio-picture-game :

mkdir twilio-picture-game

Then change into that directory, as that’s where your code will be.

cd twilio-picture-game

Since the code for this tutorial will be in Python, create a virtual environment :

python3 -m venv .venv

Activate your virtual environment:

source .venv/bin/activate

Then, using the pip package manager, install the required dependencies in your virtual environment:

pip install python-dotenv Flask quickdraw twilio polling Flask-Session

Step 4: Configure environment variables

With your local development environment set up, it’s time to configure some environment variables so your credentials are hidden. As a best practice when working with sensitive information like API keys and passwords, it’s important to ensure they are secure and not exposed to the public.

Create a file called .env in the project’s root directory (/twilio-picture-game ) to store your API keys.

touch .env

Within the .env file, create the following environment variables:

TWILIO_AUTH_TOKEN=
MY_TWILIO_NUMBER=

Next, paste your credentials within the .env file.

The Account SID , Auth Token , and Twilio Phone Number can be found on the Homepage of your Twilio Console under Account Info.

Your .env file should now look similar to this:

TWILIO_ACCOUNT_SID= ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MY_TWILIO_NUMBER=+1XXXXXXXXXX

If you’re pushing this code to a Git repository, please make sure to add the .env file to your .gitignore so that these credentials are secured. i.e., echo ".env" >> .gitignore

Step 5: Create a Service, Environment, and Asset to host pictures

This application will eventually retrieve a picture for users to guess. However, before fetching the image, create a serverless environment to host them.

To quickly build and deploy these images without having to spin up or maintain infrastructure, Serverless is a possible solution. Twilio Serverless API allows you to deploy Assets and Environments programmatically. Assets will be used to host pictures in a development Environment, and a Service is a container for your Assets and Environments.

Create a new file called serverless.py :

touch serverless.py

Within serverless.py , paste the following:

import os

from dotenv import load_dotenv, set_key
from pathlib import Path
from twilio.rest import Client

load_dotenv()

account_sid = os.getenv("TWILIO_ACCOUNT_SID")
auth_token = os.getenv("TWILIO_AUTH_TOKEN")

client = Client(account_sid, auth_token)

service = client.serverless.v1.services.create(
                                            include_credentials=True,
                                            unique_name='picture-game',
                                            friendly_name='Picture Guessing Game'
                                        )

environment = client.serverless \
                    .v1 \
                    .services(service.sid) \
                    .environments \
                    .create(domain_suffix='dev', unique_name='dev')

asset = client.serverless \
              .v1 \
              .services(service.sid) \
              .assets \
              .create(friendly_name='drawing')

dotenv_path = Path('.env')
set_key(dotenv_path, 'TWILIO_SERVICE_SID', service.sid)
set_key(dotenv_path, 'TWILIO_ENV_SID', environment.sid)
set_key(dotenv_path, 'TWILIO_ASSET_SID', asset.sid)

print('Wrote SIDs to .env file.')

Executing the command python3 serverless.py will create a Service, Environment, and Asset, and append their corresponding SIDs to additional environment variables in the .env file.

Your .env file should now look similar to this:

TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MY_TWILIO_NUMBER=+1XXXXXXXXXX
TWILIO_SERVICE_SID= ZSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_ENV_SID= ZExxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_ASSET_SID= ZHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Step 6: Quick, Draw! picture

In this step, you’ll interact with the quickdraw Python API to get a random animated drawing.

If you’d like to see the code associated with this blog post, it’s available in this GitHub repository .

Within your project directory, create a new file called quick_draw.py and paste the following code into it:

import random

from quickdraw import QuickDrawData

categories = [
    "airplane",
    "backpack",
    "bear",
    "bicycle",
    "bus",
    "camera",
    "computer",
    "drums",
    "firetruck",
    "guitar",
    "headphones",
    "helicopter",
    "skateboard",
    "tennis racquet",
    "train",
    "wristwatch"
]

def quick_draw():
    try:
        qd = QuickDrawData()
        drawing = qd.get_drawing(random.choice(categories))
        drawing.animation.save('quickdraw.gif')
        return drawing.name
    except Exception as e:
        print(e)

Here’s a breakdown of the code in quick_draw.py :

  • Lines 1-3, are module imports to make use of their functionality.
  • Line 3, uses the quickdraw module and will be used to interact with the quickdraw API.
  • Lines 5-22, create a categories list. Each item represents a category of drawings available in Quick, Draw! Select your own across the 345 available categories .
  • Lines 24-31, define a quick_draw function that pulls an image from the data set and saves it.
  • Line 27, fetches a random drawing from a random category.
  • Line 28, saves the drawing as a GIF file with the filename quickdraw.gif

Executing the command python3 -c "import quick_draw; quick_draw.quick_draw()" will run the function and save a random image, as seen below.

Animation showing the quick_draw function

Step 7: Deploy your picture using Serverless API

Next, deploy and host your GIF using the Twilio Serverless API . Create a new file called deploy.py by executing touch deploy.py. Then add the following code:

import os
import json
import polling
import requests

from dotenv import load_dotenv
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException

load_dotenv()

account_sid = os.getenv('TWILIO_ACCOUNT_SID')
auth_token = os.getenv('TWILIO_AUTH_TOKEN')
service_sid = os.getenv('TWILIO_SERVICE_SID')
env_sid = os.getenv('TWILIO_ENV_SID')
asset_sid = os.getenv('TWILIO_ASSET_SID')
asset_version_sid = None
build_sid = None

client = Client(account_sid, auth_token)

def host_asset():
    asset_version_sid = create_asset_version()
    build_sid = create_build(asset_version_sid)
    create_deployment(build_sid)


def create_asset_version():
    service_url = f'https://serverless-upload.twilio.com/v1/Services/{service_sid}'
    upload_url = f'{service_url}/Assets/{asset_sid}/Versions'

    file_contents = open('quickdraw.gif', 'rb')

    response = requests.post(
        upload_url,
        auth=(account_sid, auth_token),
        files={
            'Content': ('quickdraw.gif', file_contents, 'image/gif')
        },
        data={
            'Path': 'quickdraw.gif',
            'Visibility': 'public',
        },
    )

    new_version_sid = json.loads(response.text).get("sid")
    print(new_version_sid)
    return new_version_sid


def create_build(asset_version_sid):
    build = client.serverless \
        .v1 \
        .services(service_sid) \
        .builds \
        .create(asset_versions=[asset_version_sid])
    print(build.sid)
    return build.sid


def check_build_status(build_sid):
    build = client.serverless \
        .v1 \
        .services(service_sid) \
        .builds(build_sid) \
        .fetch()
    print(build.status)
    return build.status


def is_completed(response):
    return response == 'completed'


def create_deployment(build_sid):
    polling.poll(
        lambda: check_build_status(build_sid),
        check_success=is_completed,
        step=5,
        timeout=30
    )

    deployment = client.serverless \
        .v1 \
        .services(service_sid) \
        .environments(env_sid) \
        .deployments \
        .create(build_sid=build_sid)
    print(deployment.sid)

Here’s a breakdown of the code in deploy .py :

  • Lines 1-8, are module imports to make use of their functionality.
  • Lines 12-16, define variables taken from the .env file.
  • Lines 22-25, define an orchestrator function that invokes functions to create an asset, build, and deployment.
  • Lines 27-47, creates an Asset Version by uploading the animated GIF to Serverless.
  • Lines 49-56, creates a Build of the asset.
  • Lines 58-65, fetches the Build Status .
  • Lines 67 & 68, checks if the Build Status is completed.
  • Lines 70-84, uses polling to check the Build Status, once completed, it creates a Deployment .

Running the command python3 -c "from deploy import host_asset; host_asset()" will deploy the previously stored image to Twilio Serverless Assets.

Showing quick_draw functionality
Within the Twilio Console, you can view the deployed Asset (in this case, an animated drawing of what appears to be a computer).

Step 8: Send messages with Twilio

Programmable Messaging is Twilio’s API for sending messages using channels like SMS and MMS. Create a new file called app.py and define a function to send text messages using the Programmable Messaging API. Here’s the code for reference:

import os

from dotenv import load_dotenv
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException

load_dotenv()

account_sid = os.getenv('TWILIO_ACCOUNT_SID')
auth_token = os.getenv('TWILIO_AUTH_TOKEN')

client = Client(account_sid, auth_token)

def send_outbound_text(to=None, body=None, media_URL=None):
    try:
        message = client.messages.create(
            from_=os.getenv('MY_TWILIO_NUMBER'),
            to=to,
            body=body,
            media_url=media_URL
        )
        print(message.body)
    except TwilioRestException as e:
        print(e)
        raise

Here’s a breakdown of app.py :

  • Lines 9-10, retrieve the Account SID and Authentication Token from your Twilio account.
  • Line 12, creates a client object.
  • Lines 14-25, define a function to send an outbound text message using the Programmable Messaging API.

Test the SMS functionality by executing the command python3 -c "from app import send_outbound_text; send_outbound_text('YOUR PHONE NUMBER HERE', 'test message')".

Be sure to replace the function parameters with the recipient's phone number.

Running the script to send an outbound SMS with Twilio

Pass your personal phone number in the to parameter, which will send you a message.

Step 9: Game logic

Now that you have successfully procured a random animated drawing and uploaded it to Twilio Serverless, you can create a new file for the game logic. The game will activate on receipt from an incoming message. If the user is not playing, a new game will start (triggered by the keyword “GO”). A random drawing will be fetched and deployed. If the user incorrectly responds with the wrong drawing name, their remaining lives will decrease by one until they either make a correct guess or exhaust their 3 lives.

Modify app.py by inserting the following code:

from quick_draw import quick_draw
from deploy import host_asset

import threading
from flask import Flask, request, session
from flask_session import Session
from time import sleep


app = Flask(__name__)
app.secret_key = 'super_secret_key'
app.config['SESSION_TYPE'] = 'filesystem'

# Replace ASSET_URL with your 
asset_url = "ASSET_URL/images/quickdraw.gif"
drawing_name = None
is_playing = False

Session(app)


def initialize_game():
    session.clear()
    session['playing'] = False
    session['lives'] = 3


def start_new_game(user_number):
    initialize_game()
    session['playing'] = True
    session['drawing_name'] = quick_draw()

    countdown_thread = threading.Thread(target=send_countdown, args=(user_number))
    countdown_thread.start()

    host_asset_thread = threading.Thread(target=host_asset_with_callback, args=(user_number))
    host_asset_thread.start()


def send_asset_callback(user_number):
    send_outbound_text(
        user_number,
        'Can you guess this drawing?\n\nYou have 3 guesses remaining.',
        asset_url
    )


def host_asset_with_callback(user_number):
    host_asset()
    sleep(5)
    send_asset_callback(user_number)


def send_countdown(user_number):
    send_outbound_text(user_number, 'Game is starting soon...')
    sleep(2)

    for i in range(5, 0, -1):
        send_outbound_text(user_number, str(i))
        sleep(2.5)


def handle_bad_guess(user_number):
    session['lives'] -= 1
    if session['lives'] == 0:
        send_outbound_text(
            user_number,
            f"Sorry, you ran out of attempts! Game over. 💔\n\nThe correct answer was: {session['drawing_name']}"
        )
        initialize_game()
    else:
        send_outbound_text(
            user_number,
            f"Try again ❌. You have {session['lives']} guesses remaining."
        )


def handle_good_guess(user_number):
    send_outbound_text(user_number, 'Correct ✅! You guessed it!')
    initialize_game()


@app.route("/game", methods=['POST'])
def receive_inbound_text():
    user_input = request.values.get('Body', None).lower()
    user_number = request.values.get('From')

    if 'playing' not in session:
        initialize_game()

    if not session['playing']:
        if 'go' in user_input:
            start_new_game(user_number)
        else:
            send_outbound_text(user_number, 'To play a new game, send "GO"')
    else:
        if user_input == session['drawing_name']:
            handle_good_guess(user_number)
        else:
            handle_bad_guess(user_number)
    return '200'


app.run(host='localhost', debug=False, port=3000)

Here’s a breakdown of the modified code in app.py :

  • Lines 1-3, import the previously created functions from quick_draw.py and deploy.py .
  • Lines 10-18, set up the web application using Flask.
  • Lines 20-23, define a function to initialize the game, resetting the game state.
  • Lines 25-34, define a function to start the game, initializing the game and starting a thread to deploy a drawing.
  • Lines 36-40, define a function to send a text message with instructions to the game as well as a MMS of the drawing.
  • Lines 47-53, define a function to send messages with a countdown, like a loading screen since deploying an image can take tens of seconds.
  • Lines 55-65, define a function to handle a player’s incorrect guess, reducing their lives by one.
  • Lines 67-69, define a function to handle a player’s correct guess.
  • Lines 71-88, define a Flask route, called /game, to handle incoming POST requests and act as the orchestrator of the game.
  • Line 90, runs the Flask web application on port 3000.
View the complete app.py file on GitHub.

Execute the command python3 app.py to run the app server.

Example incoming message for the number guessing game
Users have 3 attempts to guess what the drawing is.

Step 10: Configure a webhook for responses to an incoming message

With the code for the application complete, you’ll need to configure your Twilio phone number to send requests to a webhook for handling responses to incoming messages. This configuration enables the application to receive and reply to incoming messages, completing the interactivity for the game.

At this point, running python3 app.py will start your local server on http://localhost:3000 . As of now, your application is only running on a server within your computer. But you need a public-facing URL (not http://localhost ). You could deploy your application to a remote host, but a quick way to temporarily make your web application available on the Internet is by using a tool called ngrok .

In another terminal window run the following command:

ngrok http 3000

This will create a “tunnel” from the public Internet to port 3000 on your local machine, where the Flask app is listening for requests. You should see output similar to this:

Showing ngrok in the console

Take note of the line that says “Forwarding”. In the image above, it reads: https://5bad813c2718.ngrok.io -> http://localhost:3000.

This means that your local application is accessible, publicly, on https://5bad813c2718.ngrok.io and your Flask route is accessible on https://5bad813c2718.ngrok.io/game.

Each time you run the command ngrok http 3000, a new Forwarding URL will be randomly generated.

In the Twilio Console, do the following:

  • Go to the Console’s Number page .
  • Click on the Twilio phone number you purchased earlier (or want to modify).
  • Scroll down to the Messaging Configuration section.
  • Under A Message Comes In , select Webhook .
  • Paste in the URL of the application https://5bad813c2718.ngrok.io/game.
  • Click Save Configuration .
Where to paste your ngrok URL in the Twilio console

Don’t forget to append the route /game to the ngrok URL.

After completing the configuration and ensuring your web server and ngrok are running, you can give the game a try.

Since it’s triggered by an incoming message, send a text message to your Twilio number to trigger the game. In my case, text “GO” to +1 (844) 905-5079.

Next steps

If you’d like to learn more about this game along with an introduction to Programmable Messaging, I presented it during SIGNAL 2023. You can watch the recording here.

Thanks so much for reading! If you found this tutorial helpful, have any questions, or want to show me what you’ve built, let me know online. And if you want to learn more about me, check out my intro blog post .

Anthony Dellavecchia portrait
Anthony Dellavecchia is a Developer Evangelist at Twilio who writes code on stage in front of a crowd. He is an experienced software developer who teaches thousands of people how to change the world with code. His goal is to help you build deep experiences and connections with technology so that they stick with you forever.