Build a Wordle-like SMS game with Twilio Serverless in JavaScript

Lizzie Siegle - Jan 13 '22 - - Dev Community

This blog post was written for Twilio and originally published on the Twilio blog.

I've recently become obsessed with Wordle, a word puzzle game created by Brooklyn-based software engineer Josh Wardle for his word game-loving partner. As a homage to Josh, and just-for-fun, I created a version of the game that can be played via text message. Read on to learn how to build a SMS version using the Dictionary API, Twilio Functions, the Twilio Serverless Toolkit, Twilio Assets, and cookies in Twilio Runtime, and play Twordle yourself: text a 5-letter word or "?" to +12155156567 or over WhatsApp!
text message example

Prerequisites

Get Started with the Twilio Serverless Toolkit

The Serverless Toolkit is CLI tooling that helps you develop locally and deploy to Twilio Runtime. The best way to work with the Serverless Toolkit is through the Twilio CLI. If you don't have the Twilio CLI installed yet, run the following commands on the command line to install it and the Serverless Toolkit:

npm install twilio-cli -g
twilio login
twilio plugins:install @twilio-labs/plugin-serverless
Enter fullscreen mode Exit fullscreen mode

Create your new project and install our lone requirement got, an HTTP client library to make HTTP requests in Node.js, by running:

twilio serverless:init twordle
cd twordle
npm install got@^11.8.3
Enter fullscreen mode Exit fullscreen mode

Add a Static Text File to Twilio Assets

Twilio Assets is a static file hosting service that allows developers to quickly upload and serve the files needed to support their applications. We want our Twilio Asset to be private–this means it will not be accessible by URL or exposed to the web; rather, it will be packaged with our Twilio Function at build time. For more information on Private, Public, and Protected Assets, check out this page.

Copy this GitHub file containing five-letter words from the English dictionary and add it to our Assets folder as words.private.text. We will read the file from our Twilio Function and generate a random word from it that will be used for each Wordle game. The word will be different for each person, and each person can play multiple times a day.

Write the Word Game Logic with JavaScript

cd into the \functions directory and make a new file called game.js containing the following code to import the got module, read the words.txt file from Twilio Assets, create a randomWord function to return a random word from the Asset, and initialize two constants (the user always has five chances to guess the word, and all the words are five letters):

const got = require('got');
let words = Runtime.getAssets()['/words.txt'].open().toString().split("\n");
const randomWord = () => {
    return words[words.length * Math.random() | 0];
}
const maxGuesses = 5;
const wordLength = 5;
Enter fullscreen mode Exit fullscreen mode

Next we have the meaty handleGuess function that takes in a parameter player (an object representing each player), and a guess (the word they text in as a guess.) We make a score card which will contain the boxes we return based on how close the user's guess is to the generated random word. In a try block, we make an HTTP request with got to the dictionary API using guess: if the page exists, the guess is a word, and we increment the guessesAttempted attribute of the player object. For each letter in the guess, we check if it is in the goal word: if a letter is in the same spot, that spot in the score card will contain a green square (🟩). If a letter in the guess is not in the same index as the generated word for the player but the letter is in the generated word, the score card will contain a yellow square (🟨). Else, the score card will contain a black square (⬛). If our HTTP request is unsuccessful, our score card will be a string telling the user to try again.

const handleGuess = async (player, guess) => {
  let newScoreCard = [];
  try {
    const response = await got(`https://api.dictionaryapi.dev/api/v2/entries/en/${guess}`).json();
    if (response.statusCode !== 404) {
      player.guessesAttempted+=1;
      for (let i = 0; i < guess.length; i++) {
        if (guess.charAt(i) == player.randWord.charAt(i)) {
          if (player.dupLetters[i] != null) {
            player.numCorrectLetters+=1;
          }
          player.dupLetters[i] = null;
          newScoreCard.push('🟩');
        } else if (guess.charAt(i) != player.randWord.charAt(i) && player.randWord.includes(guess.charAt(i))) {
          newScoreCard.push('🟨');
        } else {
          if (!player.incorrectLettersArr.includes(guess.charAt(i))); {
            player.incorrectLettersArr.push(guess.charAt(i));
          }
          newScoreCard.push('');
        }
      }
      console.log(`newScoreCard ${newScoreCard}`);
      return newScoreCard;
    } 
    else { //404 word not in dict
      newScoreCard = "word not found in dictionary! try again!";
      console.log('Word not found!');
      return newScoreCard;
    }
  }
  catch (err) {
    newScoreCard = "word not found in dictionary! try again!";
    console.log('Word not found!');
    return newScoreCard;
  }  
}
Enter fullscreen mode Exit fullscreen mode

After our function to handle each guess, let's make a function to check if the game is over. For parameters it accepts the player object and scoreCard. If the number of attempted guesses for the player is greater than or equal to five (the most number of guesses a player can have), the number of correct letters guessed is equal to the word length (five), or the score card contains five green squares, the game is over and endFunc returns true. Else, the game continues and returns false.

const endFunc = (player, scoreCard) => {
  if (player.guessesAttempted >= maxGuesses) { 
    console.log(`guessesAttempted >= maxGuesses`);
    return true;
  }
  else if (player.numCorrectLetters == wordLength) {
    console.log("in numCorrect");
    return true;
  }
  else if(scoreCard == `🟩,🟩,🟩,🟩,🟩`) {
    console.log(`scorecard = 🟩,🟩,🟩,🟩,🟩`);
    return true;
  }
  else {
    console.log(`game still going`);
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Call Game Logic in Twilio Functions' Handler Method

The handler method is like the entry point to your app, similar to a main() function in Java or __init__ in Python. In this tutorial, it will run each time someone texts our Twilio number. For more information on Function invocation and execution, read this page.

First in the method, we initialize a Twilio Messaging Response object to respond to the player's guess text message, a guess variable which is whatever the player texted in, a responseText string as empty text that we will append to depending on the guess, create a Twilio Response object to handle memory management with cookies,and a player object whose attributes we will initialize based on the guess.

exports.handler = async function(context, event, callback) {
  let twiml = new Twilio.twiml.MessagingResponse();
  let responseText = '';
  let guess = event.Body.toLowerCase().trim();
  let response = new Twilio.Response();
  let player;
Enter fullscreen mode Exit fullscreen mode

If the player texts in a question mark, we return a message about Josh Wardle who made the game as well as instructions on how to play the game.

if (guess == "?") {
    twiml.message(`Wordle was made by Josh Wardle, a Brooklyn-based software engineer, for his partner who loves word games. You guess a 5-letter word and the responding tiles reflect how close your guess was to the goal word. 🟩 means a letter was in the right spot, 🟨 means the letter was correct but in the wrong spot, and ⬛️ means the letter is not in the goal word.`)
    return callback(null, twiml); //no need for cookies
  }
Enter fullscreen mode Exit fullscreen mode

Then with cookies we check if the player has texted in before. If the player does not exist, we generate a new word for them and initialize a new player object with the random word, the guesses attempted (none so far), number of correct letters (none so far), an array of duplicate letters, and an array of incorrect letters guessed (currently empty.) If the player does exist, we pull data off the cookie to get the player state and make that the player object.

if (!event.request.cookies.player) { //any guesses attempted? -> new player
    let randWord = randomWord(); //new random word
    player = { //init new player
      randWord: randWord, 
      guessesAttempted: 0,
      numCorrectLetters: 0,
      dupLetters: [...randWord],
      incorrectLettersArr: []
    }
  } else { //else pull data off cookie to get player state
    player = JSON.parse(event.request.cookies.player);
  }
Enter fullscreen mode Exit fullscreen mode

We check the length of the guess and if it's five letters, we run the handleGuess method and pass it player and guess from above. We then check if the game is over and if it was a win, we send a congratulatory response; otherwise if it's a loss, we send a more apologetic message. Under both conditions, we remove the player from cookie memory to start the player over using response.removeCookie("player");.

If the game is not over, the response message is the scorecard with squares and we save the game state with the player object with response.setCookie. It's in setCookie that we also set a four-hour time limit so the user has four hours to guess before the game state is lost–the default time limit for cookies in a Twilio Function is one hour. Lastly, if the guess is not five letters-long, we tell the player to send a five-letter word.

 if (guess.length == wordLength) { //5 letters
    let scoreCard = await handleGuess(player, guess); //guessesAttempted increments
    console.log(`scoreCard ${scoreCard}`);
    if(endFunc(player, scoreCard)) { //over, win
      if(guess == player.randWord) {
        responseText += `Nice🔥! You guessed the right word in ${player.guessesAttempted}/${maxGuesses} guesses. You can play again by sending a 5-letter word to guess a new random word 👀 \nThanks for playing our SMS Twordle game. Do head to https://www.powerlanguage.co.uk/wordle for web-based word fun! Original Wordle creator Josh Wardle: as fellow builders we salute you and thank you for inspiring us to create our SMS experiment`
        response.removeCookie("player");
      }
      else if (guess != player.randWord) { //over, lose
        responseText += `Game over 🙈\nThe correct word was ${player.randWord}. Send a 5-letter guess to play again! \nThanks for playing our SMS Twordle game. Do head to https://www.powerlanguage.co.uk/wordle for web-based word fun! Original Wordle creator Josh Wardle: as fellow builders we salute you and thank you for inspiring us to create our SMS experiment`;
        response.removeCookie("player");
      }
    }
    else { //keep guessing, not over
      responseText += `${scoreCard.toString()} \n${player.guessesAttempted}/${maxGuesses} guesses`;
      response.setCookie("player", JSON.stringify(player), [
        'Max-Age=14400' //4 hour time-limit
      ]);
    }
  }
  else { //not 5 letters
    responseText += `"${guess}" is not valid. Please send a word in the dictionary that is 5 letters to get started!`;
    // twiml.message(`"${guess}" is not valid. Please send a word in the dictionary that is 5 letters to get started!`);
    console.log(`randWord ${player.randWord} in invalid `);
  }
Enter fullscreen mode Exit fullscreen mode

At the bottom of the handler method we append header, add information to our response about playing if the player has only guessed one time, send our responseText in twiml.message, and add twiml to return to our Twilio Response to both send our response text message to the player as well as update the player object in cookie memory.

response.appendHeader('Content-Type', 'text/xml');
  // see if player.guessesAttempted == 1
  if (player.guessesAttempted == 1) {
    responseText += `\nText "?" for help on how to play`
  }
  twiml.message(responseText);
  response.setBody(twiml.toString());
  return callback(null, response);
Enter fullscreen mode Exit fullscreen mode

Wow that was a lot! The complete handler method is below.

exports.handler = async function(context, event, callback) {
  let twiml = new Twilio.twiml.MessagingResponse();
  let responseText = '';
  let guess = event.Body.toLowerCase().trim();
  let response = new Twilio.Response();
  let player;
  if (guess == "?") {
    twiml.message(`Wordle was made by Josh Wardle, a Brooklyn-based software engineer, for his partner who loves word games. You guess a 5-letter word and the responding tiles reflect how close your guess was to the goal word. 🟩 means a letter was in the right spot, 🟨 means the letter was correct but in the wrong spot, and ⬛️ means the letter is not in the goal word.`)
    return callback(null, twiml); //no need for cookies
  }

  if (!event.request.cookies.player) { //any guesses attempted? -> new player
    let randWord = randomWord(); //new random word
    player = { //init new player
      randWord: randWord, 
      guessesAttempted: 0,
      numCorrectLetters: 0,
      dupLetters: [...randWord],
      incorrectLettersArr: []
    }
  } else { //else pull data off cookie to get player state
    player = JSON.parse(event.request.cookies.player);
  }

  if (guess.length == wordLength) { //5 letters
    let scoreCard = await handleGuess(player, guess); //guessesAttempted increments
    console.log(`scoreCard ${scoreCard}`);
    if(endFunc(player, scoreCard)) { //over, win
      if(guess == player.randWord) {
        responseText += `Nice🔥! You guessed the right word in ${player.guessesAttempted}/${maxGuesses} guesses. You can play again by sending a 5-letter word to guess a new random word 👀 \nThanks for playing our SMS Twordle game. Do head to https://www.powerlanguage.co.uk/wordle for web-based word fun! Original Wordle creator Josh Wardle: as fellow builders we salute you and thank you for inspiring us to create our SMS experiment`
        response.removeCookie("player");
      }
      else if (guess != player.randWord) { //over, lose
        responseText += `Game over 🙈\nThe correct word was ${player.randWord}. Send a 5-letter guess to play again! \nThanks for playing our SMS Twordle game. Do head to https://www.powerlanguage.co.uk/wordle for web-based word fun! Original Wordle creator Josh Wardle: as fellow builders we salute you and thank you for inspiring us to create our SMS experiment`;
        response.removeCookie("player");
      }
    }
    else { //keep guessing, not over
      responseText += `${scoreCard.toString()} \n${player.guessesAttempted}/${maxGuesses} guesses`;
      console.log(`randWord in not over ${player.randWord}`);
      response.setCookie("player", JSON.stringify(player), [
        'Max-Age=14400' //4 hour time-limit
      ]);
    }
  }
  else { //not 5 letters
    responseText += `"${guess}" is not valid. Please send a word in the dictionary that is 5 letters to get started!`;
    // twiml.message(`"${guess}" is not valid. Please send a word in the dictionary that is 5 letters to get started!`);
    console.log(`randWord ${player.randWord} in invalid `);
  }
  response.appendHeader('Content-Type', 'text/xml');
  // see if player.guessesAttempted == 1
  if (player.guessesAttempted == 1) {
    responseText += `\nText "?" for help on how to play`
  }
    // Add something to responseText that says: "Text 'HELP' for help" or whatever
  twiml.message(responseText);
  response.setBody(twiml.toString());
  return callback(null, response);
};
Enter fullscreen mode Exit fullscreen mode

You can view the complete code on GitHub here.

Configure the Function with a Twilio Phone Number

To open up our app to the web with a public-facing URL, go back to the twordle root directory and run twilio serverless:deploy. Grab the link ending in /game. In the phone numbers section of your Twilio Console, select a purchased Twilio phone number and scroll down to the Messaging section. Under A MESSAGE COMES IN, change Webhook to Function and then under Service select Twordle, for Environment select dev-environment, and then for Function Path select /game.

Click the Save button below and tada🎉! You can now text your Twilio number a 5-letter word to get started playing Twordle!
sms example

What's Next for Twilio Serverless, Assets, and Word Games?

Twilio's Serverless Toolkit makes it possible to deploy web apps quickly, and Twilio Runtime seamlessly handles servers for you.

Let me know online what you're building with Serverless and what your current Wordle streak is! Mine is
Wordle 208 5/6
⬛⬛⬛⬛⬛
🟧🟧⬛⬛⬛
🟧🟧⬛⬛⬛
🟧🟧⬛🟧⬛
🟧🟧🟧🟧🟧

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .