How to Make a Wordle Solver with Twilio Serverless, Studio, and SMS

Lizzie Siegle - Feb 24 '22 - - Dev Community

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

Like many word nerds and puzzle lovers, I am obsessed with Wordle, a word puzzle game created by Brooklyn-based software engineer Josh Wardle for his word game-loving partner. I made a Wordle version over SMS with Twilio Serverless to play even more Wordle, but sometimes, I get stuck while playing. Read on to learn how to build a SMS Wordle solver using Twilio Studio, Twilio Functions, the Twilio Serverless Toolkit, and the Datamuse API to find words given a set of constraints, or test it out by texting anything to +18063046212!

This was built with my coworker Craig Dennis on my Twitch channel.
sms example
Want a brief overview of how it's built? Check out this Tiktok!

Prerequisites

  1. A Twilio account - sign up for a free one here and receive an extra $10 if you upgrade through this link
  2. A Twilio phone number with SMS capabilities - configure one here
  3. Node.js installed - download it here

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 [superagent](https://www.npmjs.com/package/superagent), an HTTP client library to make HTTP requests in Node.js, by running:

twilio serverless:init wordle-solver --template=blank && cd wordle-solver && npm install superagent
Enter fullscreen mode Exit fullscreen mode

Hit the Datamuse API to Receive Potential Wordle Words with JavaScript

You can do a lot with the Datamuse API. For example, to retrieve words that start with t, end in k, and have two letters in-between, you would hit api.datamuse.com/words?sp=t??k and see:
words starting with t, ending in k, with 2 letters in-between
Make a file in the functions folder of your wordle-solver serverless project from solver.js. At the top, import superagent and make a helper function to, given a letter and a word, return indices found to later calculate black letters from the yellow squares and guesses input.

const superagent = require("superagent");
function findIndices(letter, word) {
  return word
    .split("")
    .map((l, i) => {
      if (l === letter) {
        return i;
      }
    })
    .filter((index) => index >= 0);
}
Enter fullscreen mode Exit fullscreen mode

The meat of the code is in the Function handler method:

exports.handler = function (context, event, callback) {
  // Here's an example of setting up some TWiML to respond to with this function
  let greenSquares = String(event.green.toLowerCase());
  let yellowSquares = event.yellow ? event.yellow.toLowerCase() : "";

  let guesses = event.guesses.toLowerCase().split(",");
  // Finds yellow places (right letter wrong space)
  // Looks like {'e': [4, 3], 'a': [0]}
  const yellowIndices = yellowSquares.split("").reduce((indices, letter) => {
    guesses.forEach((guess) => {
      if (indices[letter] === undefined) {
        indices[letter] = [];
      }
      const foundIndices = findIndices(letter, guess);
      indices[letter] = indices[letter].concat(foundIndices);
    });
    return indices;
  }, {});
  console.log(`yellowIndices ${JSON.stringify(yellowIndices)}`);
  console.log(`guess ${guesses}, greenSquares ${greenSquares}, yellowSquares ${yellowSquares}`);
  const blackSquares = guesses
    // To an array of arrays of letters
    .map((word) => word.split(""))
    // To a single array
    .flat()
    // Only the missing letters
    .filter((letter) => {
      return !yellowSquares.includes(letter) && !greenSquares.includes(letter);
    }); //get black squares
  console.log(`blackSquares ${blackSquares}`);
  let messagePattern = greenSquares + `,//${yellowSquares + '?'.repeat(5 - yellowSquares.length)}`;
  //let messagePattern = greenSquares + `,*${yellowSquares}*`; 
  console.log(`messagePattern ${messagePattern}`);
  superagent.get(`https://api.datamuse.com/words?max=1000&sp=${messagePattern}`).end((err, res) => {
    if (res.body.length <= 2) { //Datamuse doesn't have any related words
      console.log("no related words");
      return callback(null, { "words": [] });
    } //if
    let allWords = res.body.map(obj => obj.word);
    let wordsWithoutBlackLetters = allWords.filter(
      word => {
        return word.split("").every(letter => !blackSquares.includes(letter));
      });
    console.log(`wordsWithoutBlackLetters ${wordsWithoutBlackLetters}`);
    const withoutIncorrectYellow = wordsWithoutBlackLetters.filter((word) => {
      // for each letter in the indices
      for (const [letter, indices] of Object.entries(yellowIndices)) {
        for (const index of indices) {
          if (word.charAt(index) === letter) {
            // Short circuit (Johnny 5 alive)
            return false;
          }
        }
      }
      // It's a keeper!
      return true;
    });
    return callback(null, { 
      "words": withoutIncorrectYellow.slice(0, 10), //due to message length restrictions and these are the likeliest words
      "guesses": guesses
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

The complete code can be found on GitHub here.

Go back to the wordle-solver root directory and run twilio serverless:deploy. Copy the function URL from the output and save it for later. It will look like this: https://wordle-solver-xxxx-dev.twil.io/solver. Now the Function is deployed, but we need to make the Twilio Studio Flow that will call this Twilio Function to return possible Wordle words to the user texting in.

Make the App Logic with Twilio Studio

I tried to build this Wordle Solver solely with Twilio Functions, but Craig insisted Twilio Studio was perfectly-suited for this project. Studio is Twilio's drag-and-drop visual builder, a no-code to low-code platform. I had not used Studio that much and after seeing Craig work his magic on Twitch, I am now a Studio evangelist/convert!

Open up this gist and copy the JSON to a file - you'll need to replace a few variables (service_sid, environment_sid, and function_sid) so this makes it easier to edit.

To get service_sid, run the following command with the Twilio CLI:

twilio api:serverless:v1:services:list
Enter fullscreen mode Exit fullscreen mode

Keep adding on what you get, so from the last command (take the service_sid corresponding to our project wordle-solver), run

twilio api:serverless:v1:services:environments:list --service-sid= SERVICE-SID-FROM-THE-LAST-COMMAND
Enter fullscreen mode Exit fullscreen mode

to get the environment_sid. Then run the following command to get the function_sid.

twilio api:serverless:v1:services:functions:list --service-sid=YOUR-SERVICE-SID-FROM-ABOVE
Enter fullscreen mode Exit fullscreen mode

Lastly, replace the url with the URL of your Twilio Function URL ending in "/solver" you receive when you deploy your serverless function.

To make a new Twilio Studio flow, log in to your Twilio account and go to the Studio Dashboard. Then, click the blue plus sign and give your flow the name “wordle-solver” Click next in the setup modal, scroll down and choose “Import from JSON” from the provided templates.
import from json
Paste in the JSON (with the replaced placeholders) copied from the gist. Once you finish the setup, you should see a flowchart like the one below. Hit the Publish button at the top of the flow.
part of studio flow
When someone first texts a Twilio number (that will be configured with this Studio Flow), they will be asked what words they guessed. We check their input using a Split Based On... widget that uses regex to make sure they only sent five-letter words separated by commas and if they did, we set a variable called guesses.
transitions of split based on...widget
Else, the flow goes back to the initial Send and Wait for Reply widget to ask them what they guessed again. Then the Flow asks for their green squares and yellow squares, with similar corresponding conditional widgets using more regex. If the user sent a "!" to represent no yellow squares, we replace that with an empty string to pass to the Datamuse API to return possible Wordle words based on the green and yellow square input using a Run Function widget.
run function widget
My widget config looks like this:
run function widget config
We also pass our Twilio Function either the variables we set or the user input as Function parameters:
function parameters
We then send a message from Twilio Studio with the words returned from the Datamuse API and check if whatever the user guessed was the correct Wordle word. If it was, they receive a congrats message. Else, the Studio Flow asks what they guessed and adds it to the guesses variable in Studio before going back up to ask what their green squares are again. This flow should run until the user has solved Wordle!

Configure the Studio Flow with a Twilio Phone Number

In the phone numbers section of your Twilio Console, select the Twilio number you purchased and scroll down to the Messaging section. Under A MESSAGE COMES IN change Webhook to Studio Flow and select wordle-solver (or whatever you named your Studio Flow.)
a message comes in is configured with our wordle-solver studio flow
Test it by texting anything to your Twilio number! You can use it during your daily Wordle game or you can also make your own Wordle here.
gif of sms example of wordle solver

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

Twilio Studio blew my mind. You can use it to:

  • Handle state
  • Parse complex conditional statements with regex
  • Seamlessly integrate with Twilio Functions

Thank you so much to Craig for constantly improving the app and teaching me and so many others what we could use Studio for.

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