Build an SMS Word Guessing Game with Node.js, Express, and Twilio

August 27, 2020
Written by
Reviewed by
Diane Phan
Twilion

Build an SMS Word Puzzle with Express and Twilio.png

Have you ever wanted to bring a high tech approach to a low tech game? Well you’re in luck, because with this tutorial you’ll be able to build a basic word puzzle using Twilio Programmable SMS. Forget pen and paper - this is the twenty-first century, and when we do stuff, we do it over text!

The game works like this: Players try to piece together a word or phrase by guessing individual letters. At first, their only clue is the number of letters in the word. They keep guessing letters until they are able to guess the entire word, or until they run out of lives.

Screenshot of build your own SMS game play

Prerequisites

To follow along with this guide, you’ll need the following tools and software:

Install dependencies and generate the app scaffolding

Before you begin coding, you’ll need to install a couple of additional packages.

First, you’ll need express-generator. Express-generator is a tool that builds a basic Express application on your behalf. Install it with the following command:

npm install express-generator -g

As you build out the app, you’ll also be using the Twilio CLI, which you can install with:

npm install twilio-cli -g

Now, use express-generator to create your Express app and install any dependencies required by the package.

express guess-the-word
cd guess-the-word
npm install

Finally, use this command to install both the Twilio package for Node and the express-session package that you’ll use to manage the app’s state later on.

npm install twilio express-session

When your Express app was generated, a new folder called guess-the-word was created inside your parent directory. This folder includes all of the app’s files.

There is a file inside of the folder named app.js. Open this file in your favorite text editor.

You’re going to add two small segments of code that will import and initialize the express-sessions module.

First, at the top of the file, beneath the other imports, add this line:

var session = require('express-session');

Then, jump down several lines and look for the line var app = express(); Below this line, add the following code to initialize express-session.

app.use(session({ secret: 'twilio rocks', resave: false, saveUninitialized: false}));

In a production app, you would load the secret string from a .env file, and not use the default 'twilio rocks' secret provided above.

Your completed app.js file should look like this:


var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

app.use(session({ secret: 'twilio rocks', resave: false, saveUninitialized: false}));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Save and close this file, you won’t need it again.

Woohoo! At this point you can start your local server and run your app! Back in your command prompt, start the server by running:

npm start

Congratulations, your app is now up and running! Press ctrl + c at any time to stop the server.

GIF showing how to start local server in command prompt

Create the app’s route

Awesome job so far! Now that your app can run, it’s time to create a route that will process the SMS messages your app receives.

Inside the guess-the-word directory there is a sub-directory called routes. Inside routes is a file called index.js. Open this file in your favorite text editor.

You should see some code that looks like this:

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

This is a stock route that was created for you by express-generator. Delete only the route code above and replace it with these lines:

const MessagingResponse = require('twilio').twiml.MessagingResponse;

// Creates a route at /play-game that Twilio will POST to
router.post('/play-game', (req, res) => {
  // creates a new instance of the MessagingResponse object
  const twiml = new MessagingResponse();

  // sets the message we want to respond with 
  twiml.message('Hello, can anyone hear me?')

  // sends the response back to the user
  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(twiml.toString());
});

This code imports the Twilio package, creates a new message, and sends that message back to the user.

After adding this code and saving your file, be sure to restart your local server so that the changes take effect.

Set up the webhook

The app works by responding to SMS messages sent from the user. The process looks like this:

  • An SMS is sent to your Twilio number
  • Twilio captures that SMS and sends a POST request to the route you just created
  • The app receives that message and responds.

But that can’t happen if Twilio doesn’t know where to send the POST request!

To solve this, you’ll need to set up a webhook using the Twilio CLI.

Create a new window or tab in your command prompt and login to the Twilio CLI:

twilio login

This will prompt you for the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN found in your Twilio console.

After logging in, run the following command, taking care to replace yournumber with your Twilio phone number, using the E.164 format.

This command creates the webhook and spins up a tunnel to your local server so you can test what you’ve done so far. This means that your computer is going to be temporarily receiving requests from the internet. This is how you’ll be able to test your app from your SMS-capable device.

twilio phone-numbers:update "+yournumber" --sms-url="http://localhost:3000/play-game"

You should see feedback from the Twilio CLI that looks like this:

Image showing response from Twilio CLI

Double check that your local server is still running in your other command prompt tab or window.

Test out your work by sending an SMS with any message to your Twilio number.

You should receive back the message we added to our twiml object in the previous step: “Hello, can anyone hear me?”.

Add the game logic

Congrats, you’re almost there! Now that your app can respond to SMS messages, all that’s left is adding the game logic to your /play-game route inside index.js.

First, delete these lines from your route:

 // sets the message we want to respond with 
 twiml.message('Hello, can anyone hear me?')

This code was used to test your app, but you no longer need it.

Capture and sanitize the incoming message

Capture the incoming message that comes to your app via the webhook. Then sanitize it by removing any extra whitespace at the beginning or end of the message and by converting the entire message body to lowercase.

To do this, add this line to the top of the route, after the line where you created the twiml object.

const incomingMsg = req.body.Body.toLowerCase().trim();

Set the mystery word

Next, create a variable and assign to it a word for your users to guess. If you were to eventually deploy this app, you could modify this code to use an API that generates a random word, or create an array of potential words that the app chooses from.

To keep things simple, you can make this value static.

// could be any lowercase word!
const word = 'twilio';

Create the game logic

And now for the game logic! This is when you’ll add the code to tell your app how to make decisions. This code goes inside the /play-game route, before sending the message back to the user. As you move through this section, notice that this game logic code calls a number of helper functions. Don't worry, you’ll add these helper functions in the next section!

First, when the app receives an SMS, it checks to see if a game is currently in play.


/* 
 * Game play logic 
 */

if (!req.session.playing) {
  
} else {

}

If a game is not in play, the app checks to see if the user has sent the word “start” or an invalid word.


/* 
 * Game play logic 
 */

if (!req.session.playing) {
  if (incomingMsg == 'start') {
    handleNewGame();
  } else {
    handleInvalidSMS();
  }
} else {

}

If a game is in session, the app will assume that the message is a guess and checks for the following outcomes:

  • The guess matches the mystery word
  • The guess matches a letter within the word
  • The guess doesn’t match anything

/* 
 * Game play logic 
 */

if (!req.session.playing) {
  if (incomingMsg == 'start') {
    handleNewGame();
  } else {
    handleInvalidSMS();
  }
} else {
  const winOrMatch = checkForSuccess();
}

If the guess was unsuccessful, the app will handle the bad guess by removing a life and checking to see if the player has lost.


/* 
 * Game play logic 
 */

if (!req.session.playing) {
  if (incomingMsg == 'start') {
    handleNewGame();
  } else {
    handleInvalidSMS();
  }
} else {
  const winOrMatch = checkForSuccess();

   if (!winOrMatch) {
      handleBadGuess();
   } 
}

If the guess resulted in a mystery word match, then the app processes this as a win.


/* 
 * Game play logic 
 */

if (!req.session.playing) {
  if (incomingMsg == 'start') {
    handleNewGame();
  } else {
    handleInvalidSMS();
  }
} else {
  const winOrMatch = checkForSuccess();

   if (!winOrMatch) {
      handleBadGuess();
   } else if (winOrMatch == 'win') {
      handleGameOver('You guessed the word! You win!');
   }
}

Otherwise, the app processes the guess as a matched letter. In this case the app will modify the series of underscores so that any correctly guessed letters are visible.


/* 
 * Game play logic 
 */

if (!req.session.playing) {
  if (incomingMsg == 'start') {
    handleNewGame();
  } else {
    handleInvalidSMS();
  }
} else {
  const winOrMatch = checkForSuccess();

   if (!winOrMatch) {
      handleBadGuess();
   } else if (winOrMatch == 'win') {
      handleGameOver('You guessed the word! You win!');
   } else {
      handleMatch();
    }  
}

Add the helper functions

The game logic above called several helper functions that handled the nitty gritty of the app’s functionality. Each of these helper functions will be added to your route above the game logic code you created in the previous step.

First, the handleNewGame() function. The app’s state is handled with Express sessions. When a new game begins, the app sets three pieces of session data: the beginning word state, the number of lives the user has, and a flag indicating that a game is now in session.

/* 
 * Helper functions to handle game play 
 */
  
const handleNewGame = () => {
  req.session.wordState = new Array(word.length).fill('_');
  req.session.lives = 5;
  req.session.playing = true;
  twiml.message(`Text back one letter at a time to try and figure out the word. If you know the word, text the entire word!\n\nYou have ${req.session.lives} lives left. \n\n ${req.session.wordState.join(' ')}`);
}

The second helper function is handleInvalidSMS(). This function is called when a user sends a message that says anything other than “start” outside of game play.

/* 
 * Helper functions to handle game play 
 */

const handleInvalidSMS = () => twiml.message('To start a new game, send start!');  

Next is the checkForSuccess() function. This function returns 'win' if the user’s guess matched the mystery word, 'match' if the user’s guess matched a letter within the mystery word, or false if the guess was not a match at all.

/* 
 * Helper functions to handle game play 
 */

const checkForSuccess = () => {
  if (incomingMsg == word) { return 'win' }
  if (word.includes(incomingMsg)) { return 'match' }
  return false;
}

Next, the handleGameOver(msg) function destroys any existing session data thereby ending the game play. It also receives a string message to set as the message of the twiml object.

/* 
 * Helper functions to handle game play 
 */

const handleGameOver = msg => {
  req.session.destroy();
  twiml.message(msg);
}

The next helper function, handleBadGuess(), removes a life. Then, it checks to see how many lives are remaining. If there are no lives left, the game is over, and the handleGameOver() function is called. Otherwise, it sets a message to send to the user.

/* 
 * Helper functions to handle game play 
 */

const handleBadGuess = () => {
  req.session.lives--;
                
  if (req.session.lives == 0) {
   handleGameOver('Oh no, you ran out of lives! Game over.');
  } else {
    twiml.message(`Nope, that was incorrect. You have ${req.session.lives} lives left.`);
  }
}

Finally, the handleMatch() function is used to update the word state. Then it checks to see if the word is now completely guessed. If the word has been fully guessed, the function calls the handleGameOver() function, otherwise, it sets a response to the user with the updated word state.

/* 
 * Helper functions to handle game play 
 */

const handleMatch = () => {
  for (let [i, char] of [...word].entries()) {
    if (char == incomingMsg) {
      req.session.wordState[i] = incomingMsg;
    }
  }
  
  if (req.session.wordState.join('') == word) {
    handleGameOver('You guessed the word! You win!')
  } else {
    twiml.message(req.session.wordState.join(' '));
  }
}

To see the completed code for the index.js file, check out the github repository.

Test the app

Huzzah! Your game is ready to be played!

In order to play a round, make sure both your tunnel and your local server are running.

To begin the game, send an SMS to your Twilio number that says “start”. Continue to guess letters one by one until you’re able to guess the entire word or until you lose. After a game ends, begin a new round by sending the word “start” again.

If at any point you get an error, visit the URL provided in the command prompt tab where you’re running your tunnel connection. This will take you to a debugger!

Screenshot of twilio CLI command line response with URL highlighted

Next steps

Want to keep working on this project? Try developing your app further so that the mystery word is different every time you play. Build in more error handling and deploy it so it’s live forever, no more boring road trips! Try to build this with a different Twilio API like Programmable Voice!

You could also try out another SMS game built with Twilio.

Most importantly, have fun and keep building :)

Ashley is a JavaScript Editor for the Twilio blog. To work with her and bring your technical stories to Twilio, find her at @ahl389 on Twitter. If you can’t find her there, she’s probably on a patio somewhere having a cup of coffee (or glass of wine, depending on the time).