Build a Wordle-like SMS game with Twilio Serverless in JavaScript
Tags: games, sms, serverless, javascript
Published: 2022-01-13 09:50:24

---
title: Build a Wordle-like SMS game with Twilio Serverless in JavaScript
published: true
description: Learn how to build Twilio's just-for-fun homage to Wordle builder Josh Wardle. Completed your web Wordle for the day? Keep busy until the next official Wordle via text!
tags: games, sms, serverless, javascript
cover_image: https://lh4.googleusercontent.com/AlMAvmy6CqN5PK6Zjmzse3W4Z1dkAP6ApqeBvoTs1JLf2ybxlhVu0N5OTjeSGD0OEesjohr2t3unL9oza4TBuQab7ON6kwDu5KREF6fvoPO-Lsl9q82ApgJ8AYE8kqQLIVqaJAim
---
This blog post was written for [Twilio](https://www.twilio.com/) and [originally published on the Twilio blog](https://www.twilio.com/blog/build-wordle-like-sms-game-serverless).

I've recently become obsessed with [Wordle](https://www.powerlanguage.co.uk/wordle/), a word puzzle game created by Brooklyn-based software engineer [Josh Wardle](https://twitter.com/powerlanguish) 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](https://dictionaryapi.dev/), [Twilio Functions](https://www.twilio.com/docs/runtime/functions), the [Twilio Serverless Toolkit](https://www.twilio.com/docs/labs/serverless-toolkit), [Twilio Assets](https://www.twilio.com/docs/runtime/assets), and [cookies in Twilio Runtime](https://www.twilio.com/docs/runtime/functions/headers-and-cookies/setting-and-modifying#cookies), and play Twordle yourself: <strong>text a 5-letter word or "?" to +12155156567 or over [WhatsApp](wa.me/552140423246)!</strong> 
![text message example](https://lh3.googleusercontent.com/MHgxLN_RSaSBIfqtKBYRTmmbEuELZfESylhT9soxJ1jzYUNklgaOjovir6IW6aCE-tyGq3WpYf87oLskKFpSeQb7NhnidKZ4XJLUe3c3qkGghiKF3i2HZFPcmdxOPd9C_kgNczPS)

### Prerequisites
- A Twilio account - [sign up for a free one here and receive an extra $10 if you upgrade through this link](http://www.twilio.com/referral/iHsJ5D)
- A Twilio phone number with SMS capabilities - [configure one here](https://support.twilio.com/hc/en-us/articles/223135247-How-to-Search-for-and-Buy-a-Twilio-Phone-Number-from-Console)
- Node.js installed - [download it here](https://nodejs.org/en/download/)

### Get Started with the Twilio Serverless Toolkit
The Serverless Toolkit is CLI tooling that helps you develop locally and deploy to [Twilio Runtime](https://www.twilio.com/docs/runtime). The best way to work with the Serverless Toolkit is through the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart). 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:

```bash
npm install twilio-cli -g
twilio login
twilio plugins:install @twilio-labs/plugin-serverless
```

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

```bash
twilio serverless:init twordle
cd twordle
npm install got@^11.8.3
```

### 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](https://www.twilio.com/docs/runtime/functions-assets-api/api/understanding-visibility-public-private-and-protected-functions-and-assets).

Copy this [GitHub file containing five-letter words from the English dictionary](https://github.com/elizabethsiegle/wordleSMS/blob/main/assets/words.private.txt) 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):

```javascript
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;
```

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.

```javascript
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;
  }  
}
```

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.

```javascript
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;
  }
}
```

### 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](https://www.twilio.com/docs/runtime/functions/invocation).

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.

```javascript
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 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.

```javascript
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
  }
```
Then [with cookies](https://www.twilio.com/docs/runtime/functions/headers-and-cookies/setting-and-modifying#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.

```javascript
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);
  }
```

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.

```javascript
 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 `);
  }
```

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.

```javascript
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);
```

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

```javascript
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);
};
```
You can [view the complete code on GitHub here](https://github.com/elizabethsiegle/wordleSMS).

### 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 <em>twordle</em> 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 <em>Messaging</em> section. Under <em>A MESSAGE COMES IN</em>, change <em>Webhook</em> to <em>Function</em> and then under <em>Service</em> select <em>Twordle</em>, for <em>Environment</em> select <em>dev-environment</em>, and then for <em>Function Path</em> select <em>/game</em>.

Click the <strong>Save</strong> button below and tada🎉! You can now text your Twilio number a 5-letter word to get started playing Twordle!
![sms example](https://lh5.googleusercontent.com/011zsXJX9ZS54CzpbTfrcoFZAOpeKfVOVoHAuEzafGOBOWaag0v_KD-URCKfkb12d6suw5dbWzjmorS242lK5eYHb-jQQOVMNULloQ8NzRv9Q-5eIsUY4-fG406A2cUc8wB6kJp6)

### 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
⬛⬛⬛⬛⬛
🟧🟧⬛⬛⬛
🟧🟧⬛⬛⬛
🟧🟧⬛🟧⬛
🟧🟧🟧🟧🟧
- Twitter: [@lizziepika](https://twitter.com/lizziepika)
- GitHub: [elizabethsiegle](https://github.com/elizabethsiegle)
- Email: lsiegle@twilio.com
- Livestreams: [lizziepikachu](twitch.tv/lizziepikachu)