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!
Prerequisites
- A Twilio account - sign up for a free one here and receive an extra $10 if you upgrade through this link
- A Twilio phone number with SMS capabilities - configure one here
- 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
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
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;
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;
}
}
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;
}
}
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;
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
}
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);
}
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 `);
}
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);
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);
};
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!
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
- GitHub: elizabethsiegle
- Email: lsiegle@twilio.com
- Livestreams: lizziepikachu
Top comments (1)
C'est un article fascinant. Avec Twilio Serverless et JavaScript, vous pouvez créer un jeu SMS de type Wordle. Cependant, je suis un grand fan les jeux de casino. Un certain nombre d'avancées ont été faites dans cette industrie afin d'améliorer l'expérience de jeu. Lisez le blog si vous voulez en savoir plus à ce sujet.