Resources

Winston Chang || Part I: Build a Basic Wordle App with Shiny || RStudio

00:00 Introduction 00:12 What is Wordle? 00:36 The Wordle app we'll build by the end of this four-part series 01:08 How to approach the problem 01:38 Word list (link to file below) 01:52 UI function with fluidPage() 02:24 Print out what player guesses using verbatimTextOutput() 03:36 Run app in Viewer Panel 04:04 Adding an action button with actionButton() 04:29 Using bindEvent() with actionButton() 06:02 Limiting guesses to words with five characters 07:40 Using req() and cancelOutput() 08:54 Incorporating the word list 10:13 Matching player guess to word list 11:06 Matching player guess to target word 13:50 Writing a function to match guess to target word with feedback 18:15 Checking word length between guess and target 23:02 Why we're using intermediary functions 28:51 Printing formatted letter information In Part I of this four-part series, Winston walks through how to build a basic Wordle app using Shiny! Code + word list: https://github.com/wch/shiny-wordle Check out the full Shiny app here: https://winston.shinyapps.io/wordle/ You can learn more about Shiny here: https://shiny.rstudio.com/ Got questions? The RStudio Community site is a great place to get assistance: https://community.rstudio.com/ Content: Developer (@winston_chang) Animation, design, and editing: Jesse Mostipak (@kierisi) Wordle: https://www.powerlanguage.co.uk/wordle/

image: thumbnail.jpg

Transcript#

This transcript was generated automatically and may contain errors.

Alright, well today we're going to talk about making a Wordle app, or an app like Wordle I should say. Basically, you type in a word, that's five letters, and then you hit enter, you click a button, and then it checks how many of the letters in that word are correct or are present in the target word, and then it prints that out, and then you enter in another word, and you just keep going. So that's a pretty simple thing.

So for our Wordle app, our overall strategy is going to be to first work on the logic of the application, and get that all working. And then once that's done, we will make it look pretty using some CSS and a little bit of JavaScript. And we'll do that in a later video in this series. What you're seeing here is the actual Shiny Wordle app at the end. And this does take a little bit of CSS and JavaScript work, but it can be done.

So you know the way that I like to get started with problems like this is to just start with something really really basic, and then build it up. So there's some problems where that's not really the appropriate approach, but for something like this, I think that's the way to go. So first we'll work on the logic, and then later on we can work on making it look nice. But the first thing is to get, you know, the game logic working properly.

Setting up the basic UI

Alright, so got our studio started, and I also already have my word list here. So I won't go into the depth about how to do that, but it's there. So let's start with by making a simple Shiny app. Library Shiny. The UI is, let's start with a fluid page, and I'm just gonna put in a text input for now. Right, so that's the thing that help people interact with it, is they type in text into an input. Text input, collect guests, and actually we won't put a label on it.

And all right, there's that, and server, input, output, and, oh actually, sorry, the first thing I'll do is not just have people type in input, but I'll just have it print out what they typed. So we'll do a verbatim text output. Let's load up Shiny so we can get some autocomplete. Verbatim text output, um, call it result. Oh, and also a little tip, we'll do placeholder equals true.

Okay, so then that will just make it so the text output is always visible. Otherwise, when there's no content in it, it's not, it's not visible. So that, um, it's sort of, it can be a little bit surprising if it, uh, sort of appears out of nowhere, or disappears when there's no content there. Okay, so, uh, let's see, let's do output, result, and we're, for right now, we're just going to have it echo the text that the user types in. Under text, uh, we'll do input, input, dollar, guess, and then shiny app, log, server. Okay, so now RStudio recognizes it's an app, recognizes it's an app, and, um, just for convenience, since this is a little thing, I want it to show up in the little viewer panel down here. So, oh, I already have it set up that way, and I'll click run app, and here's my app. Hello, it shows hello there. All right, so that's, that's the very start of our, our Wordle app.

Adding an action button with bindEvent()

So right now, it's a little bit, you know, every time I type anything, it's going to show it down here, but I only really want it to do something with the input, um, when I say I'm ready. So I'm gonna, and I'm gonna do that by adding in a button. So action button, and we'll say, call it, uh, go, and we'll label it go, and now we have this render text here. Um, there's actually a number of ways we can, we can do this, but, uh, I'll show you, I'll show you one of the sort of relatively new ways of doing it. I can edit, I can use bind event. So, um, if you're familiar with, like, event reactive, or, uh, observe event, you can have those things run only when, uh, a particular, you know, when some other reactive thing happens. So, like, when, when a button is clicked, and so you can actually use bind event with reactives and observers, but you can also use it with these render functions. So I can say bind event input $go. Okay, so it will only run, it will only sort of, it'll only update this, and, uh, or execute this and update the result text when the go button is pressed.

And I still haven't figured out the best way to do indentation for this, so it looks a little funny here. And I'm using the new R, R 4.1 style pipe here, since I'm running that version of R, and I'll save it and reload. So I can say hello, and nothing should happen until I press go. Say goodbye. Again, nothing happens, I'll press go. And now, and it's just replacing the content of this, uh, this text output right now. So that's, um, so now is a good time to start doing something, uh, with, with that.

Limiting guesses to five characters

Okay, so let's see, um, one thing that, let's see, how do we do this? So in, in Wordle, all of your guesses are limited to, uh, words that are five characters. And, and actually, on top of that, they're select, they're actually limited to words that are in a particular word list. But let's, let's just start off by, um, by limiting it to five characters first, and then we'll, we'll start, we'll make use of the word list after that.

So let's see, so we can take input$guess, and let's say, um, if nsharv input$guess is not equal to five, then, well, we'll, I'll show the, the sort of the naive way of doing it first, and then do something a little bit more sophisticated. And then, so if it's not five characters, then we'll just return out of this render text function, return nothing, or actually maybe we should return empty string.

And if it is five characters, then we move on, and, um, I'll have it, uh, show this text. So paste, you know, you entered input$guess. All right, reload, so I can say, um, abc, and if I click on here, nothing should happen, but if I say hello, which is five characters, it'll say you entered hello. And if I say goodbye, which is seven characters long, um, well, it's not printing out you entered goodbye, but it is just making the, the result disappear. So that's actually not really what we want either, um, and there is a function in Shiny called rec, like for, short for require, um, and you can use it to exit out of here and not replace the previous, um, the previous contents of, of this output. So we can say rec false, um, and, okay, actually normally if we just do rec.recFalse, and we do this, so if I say hello, goodbye, it will still, like, it'll still clear this, but what we need to say is recFalse cancelOutput equals true. So cancelOutput means, um, when you, when you hit this line of code, um, don't modify any downstream outputs that are from this, and this actually isn't an output itself, but if, if this were, like, in a reactive expression that was consumed by, um, by an output, then it would, that output would also not change. So let's say hello again, and um, goodbye, and when I click on this, now it's, it's not erasing the previous contents of this render text. Okay, great, and, uh, now we can compare it to, I guess we can compare it to our word list.

Incorporating the word list

So let's, at the top of this app, let's say source word list, okay, and, um, in this word list there's actually two sets of words. There's, um, there's common words, which are, there's, like, that's a list of, like, 2,500 words, um, which are relatively common, or the more, most common 2,500 words in, in English, um, and then there's a larger list of words that are, like, all five-letter words. Um, the shorter list is ones that are, um, I guess the target words, and the longer list are, is this, is a set of words that we can, that users can use to guess, but, but some of them are really obscure, and it would be really, really hard if those were the target words. So, oops, let's source word list dot r, and the, let's see, actually, let's just run that, oops, let's put that there, source word list dot r, okay, we have words common five, and words all five, so those are, those are the two character vectors that we have that have, contain all the words.

So, so right now, we, we just want to, um, right now, we just want to show, right now, we just want to move on from here, uh, if it's, if the, the word that they entered is an actual word in the, in the words all list. So we can say if input dollar guess is in words all five, actually, sorry, take out this parenthesis there, if it's not in words all five, then cancel output. Okay, so let's run it, and again, hello should work, but unless we just type in some random five characters, that should have no effect. Okay, so that's pretty good.

Selecting a target word

Now we can actually start, you know, we can have, make up a, we can use a target word and start, uh, processing people's guesses for that target word. Uh, let's see, so, um, we can do it, well, we can do it inside the server function. If we do this, if we select a random word inside the server function, that means that each time somebody connects to this, they'll get a different word. Um, and if we, if we select a random word outside of the server function, then it will, as long as this app is running, uh, and you don't, like, restart R or quit the app or something, or, I mean, close the app from the R side, then, um, everyone will get the same word. So that's, uh, I mean, let's do it that way.

So there's words common five, so we can just say target is sample words common five one. I think that's the right sample. Yep, just sample one of them. So, and actually, you know, of course, we can always just run this at the console. I've got to stop the app first. Yeah, so that picks out one word, duchy. That would be really hard.

You know, yet another thing you could do is do, like, uh, set the random seed based on the date. Um, you know, I don't know, you'd probably do, like, as integer, you know, sys.date. Is that, okay, so yeah, so let's, let's try this, and then, oops, well, I guess we can print it out. So for today, the target word is gives, and then if we run this all again, so, like, let's say the, the app, you know, shuts down, or let's, if you're running, you know, if you're running this on, um, if you're running this on a server where there's high load and you want this to be served up by multiple different R processes, um, then you want this to be repeatable. So let's run this again, and again, if we run target, it still gives. So, so anybody who connects today would have that word, and I guess, you know, that actually works. Let's, let's, let's use that, let's use that method for now. Okay, so, um, yeah, so every, so today the target, the word is gives, and we're just using that by setting the random seed using today's date.

Writing the word comparison function

All right, so let's see here, um, okay, so now we want to compare, we want to do some sort of comparison to say, hey, is the, is the word that the person entered, if they make it past this, this part there, like, how, how well does that match up with the target word? And that's something where it sort of, it makes a lot of sense to put that in its own function. And I will do that, um, I'll do that below the server function. So let's say, um, let's see, what should we call this? Check word function, and then we'll say, uh, the target word, and the guess.

All right, so, so what we want to say, check is, you know, not just are these two words the same, if that, if it was that simple, we wouldn't need to make a function for it, but, in Wordle, we need to know, like, how many, like, which of these letters is in the guess, is in the target word, in the correct location, and which of those letters are in the target word, and, you know, not in the, not in the correct location. So those are two things that we need to, that we want this function to figure out. And so we're, we're actually going to want this to return, um, I guess it could be, it could be a character vector or a list with, like, the status of each, each, uh, character in the, in the guess.

So, for example, if, so let's just, let me just sketch this out here. So if, you know, the target is, um, gives, that's today's target word, and the guess is, um, okay, if the guess is aisle, then let's go through each of these letters here. So what we want to tell the user, the feedback that we want to give them, is that A is not in the target word, I is in the target word, and it's in, it's in the correct location, S is in the target word, but it's in the wrong location, right here, L is not in the target word, and E is in the target word, but in the wrong location.

And, um, and in, you know, in the real Wordle game, those are, it shows that with colors, shows that with colors, um, and for right now, what we're going to do here, is we're just going to sort of mark each letter from the guess, and we're going to, we're going to return some information about it. So let's see, um, yeah, so what we want to do is, we want to iterate over each letter in the target, uh, and guess words, and, and do a comparison. So first thing to do is to, uh, well, should we do it this way? Yeah, so the first thing we can do is, uh, split those words into a character vector of letters. So if I say, um, actually, let me say, let me change this target, okay, this is just going to be kind of a long variable name, target string and guess string. So we can say that the target, this is going to be a character vector, we'll say string split, restore split, uh, target string, and if I just give it an empty string here, it'll split it every character up.

And so let's, let's try this real quick, actually. So we can just do this in the console, just to try things out. So if I say string split gives on an empty string, that will return this, well, it actually returns a list that contains a character vector with the individual letters. So what we want to do then is extract the first element of that list. So let's hit that again, and now this is what we want, we want a character vector with those letters. Okay, all right, and the same thing for the guess, so let's copy that, guess, guess, okay. And then, and then after this, now we can, we can iterate over each of the letters and compare them.

So actually, one more thing that we should do ahead of time, we need to make sure, we should make sure these, these words are the same length, just in case. I mean, the logic in, uh, that we had up above, uh, should make sure that, you know, both words are five letters, but just in case we want to, um, we should check it here, uh, and, and throw an error if they're not the same length. So if nchar target string, string is not equal to nchar guess, let's say stop target and guess string must be the same length. Be the same length. Okay, so once we get down here, we know they're the same length, and then we can iterate over, uh, over each, over the words.

All right, so let's say for, and we can do this with, uh, we can do this with a simple for loop. There's other things you can do, like you could use mapply, but in this case we can just use a for loop. So let's say for i in, uh, seek a long target, so that should be five, that should be one through five, actually. We're going to do something, and and letter. So let's say if guess i is equal to target i, then we're going to do something. And let's see, we need to, well, if they're the same, then that's, we need to sort of mark that, and we need to save that somewhere. So let's, uh, let's actually create another place to store this information. So we'll call this result, and we can say character vector. Oh, and actually, and we can pre-allocate this character vector. You know, for this, for these really small things, like performance isn't really a big issue, but if you're working with larger data, then it, it can help to pre-allocate the vectors. So I'll just do that now, even though in practice for this it probably doesn't actually matter. But let's just say nchar guessString.

Okay, so, so again, let's, well, if I just, we can run it over here in the console, and just see what that looks like. So if I say character five, it creates a character vector with five empty strings in it. Yeah, so that, that lets us assign things into there without growing the vector in a loop, and that, because that can lead to performance problems. But again, probably doesn't matter here. So let's see, so if the guess and the target letters are the same for a particular position, then let's say result i is, let's call it correct. We can say else if, so let's see if they're, yeah, if the guess and the target are the same, that's good. That's correct. But we also want to mark if the guess letter is in the target word, but in the wrong, in a different location. So we can say if guess bracket i is in target, then result i is, let's say, in, inward. Okay, and if it's not, then we'll say result i is not inward. Okay, so we're just gonna collect those results and return it. Results.

Let's see, there we go, there's the end of our function. So I'm gonna hit, just hit command enter to run this, and then we can test it out. So let's say check word gives, that's the target, and isle is our guess. So let's go through these. So the first one, a is not in the target, i is in the correct position, s is in the word, l is not in the target, and e is in the target. So that's, all right, that's great.

Yeah, just, you know, to me it just feels like this is the right size, like this, this is a function that just does, it's one thing, it tells me, it tells me whether each letter is correct, and, you know, I can build on top of that, I can use that information to see if, to check if everything is, you know, if the entire word is correct.

Yeah, just, you know, to me it just feels like this is the right size, like this, this is a function that just does, it's one thing, it tells me, it tells me whether each letter is correct, and, you know, I can build on top of that, I can use that information to see if, to check if everything is, you know, if the entire word is correct. So maybe actually, maybe I should say this is, this is compare words, okay, and, and then I can wrap, I can use another function that uses this, and then it provides some, even some more information, like whether or not their guess was correct. So, so let's say, let's make another function called check words, okay, and say it's going to actually take the same inputs, but what I'm going to do is I'm going to return a data structure that has a lot of useful information in it.

So let's say, actually, do I want to do this? Yeah, okay, let's, let's do this. Okay, so, so I'm going to say, okay, so compare result, using some long variable names, is compare words of these things, and then, let's see, if, let's say all compare results equals, if they're all correct, then we know that, that then, then the person has, has finished the game. There, they've, they've won, right? So, let's see, I'll call, I'll say correct, it's true. So let's start with the default valid, correct, it's false, actually, yeah, let's do it this way, and then we can return a list where the guess was, guess string, this is the guess word, the result for each letter is, is this compare result, and correct is correct.

Okay, so we can, we can again do check words, gives, and iel. So this is a list, the guess was iel, and the result for each comparison is here, and this is not the correct word. But if we were to check gives against gives, yep, should be, gives is here, the result for each letter is here, and correct is true. And actually, I'm going to rename this to, oh, I don't know, result, guess, okay, never mind, let's, actually, here, I'm going to give another field called letters, and that's going to be these letters from the guess split up, because that may be useful later.

Okay, does it need a comma there? Oh, guess string, okay. All right, so that's the, that's the word, maybe I should call this word, letters, result, and it's correct. Okay, so this, this actually may be more richer, this might be more and richer information than we actually need later, but it doesn't hurt to have that information now.

Formatting and displaying the result

Okay, so okay, so we've written our function to compare these words, and let's see, so we can say you entered, well, actually, let's, let's just take this and check words, target, input, dollar, guess. Now, this is going to return a list, and the render text is probably not really going to like that, but what we can do is do render print. Well, actually, let's, let's, first, let's see what happens when we do it. It's not going to, probably not going to be great. So, again, we know the word, the target word is gives, and we're guessing aisle. Yeah, so it's trying to, it's, it's, render text is expecting a string to be returned, but we're returning a list with a bunch of stuff in it, so that's, it doesn't like that, but what we can do instead is use render print, and what render print will do is it'll take this result as if you printed it at the console, like here, and then it'll, it'll display all that information. Oops, got to reload, so aisle, go. All right, and yep, so this is the information that we need, and let's say gives, that is the correct word, correct, correct, correct, correct, okay, great. So now we, now, you know, we're doing the, sort of, the proper analysis on the, on the word, but now we want it to print it in a nicer way.

So I think, so one thing, one way we can do it is, for right now, is we can print it out with, like this, I'll demonstrate, like putting brackets and parentheses around the letters. So if the target word, or if the user has entered aisle, so a, let's see, let's say a, oops, we can put, if the letter is correct, we can put square brackets around it, and if it's in the word, but in the wrong location, we could parentheses, let's see, and the l is, doesn't have anything, and e would also have parentheses. So we can, we can print it out this way for now, so the user has some information about which letters are correct.

All right, so let's do that, and again, this is, we could put it right in line here in this render print, but this is also, this is also a good place to, you know, to make a function to do the formatting. So let's say, let's say format, guess, yeah, format guess, so let's check words, call this result, or maybe format, yeah, format result. So these are not, these are not great function names. Naming things is hard, and you know, when you're working on something that's small, you can sort of get away with, with using, just sort of moving quickly, and just picking a name, and, and going on from there, but when you're working on like much, you're working on bigger projects, it's more important to have good names, because if you don't, then you'll get really confused. But this is a small enough thing that we can, we can be a little bit lazy about that.

Okay, so I'll just define the function here, format result function, this thing called result, actually, let me call it, oh, guess data is a little bit long, but gdata, or actually, let's call it r. Let's call it r. So, so we want to, for each letter here, we want to look at this correctness, the result, wow, yeah, r$result. Again, okay, this is, these are bad names, but we can, it's, it's, this is small enough that we can hold it in our heads. So we'll, we'll just iterate over each, over these, the letters, and the result here, and you know, put the proper brackets or whatever around it. So, all right, let's do a for loop again, for i in, let's see, along letters, and we know that letters and results are the same length, so we don't have to, we don't have to do any checks there. So let's see, so if, oh, I'm sorry, oh, that's right, that's right. So if we could say, if r$result, i, actually, this should be r$letters, okay, if r$result bracket i is correct, parentheses there, then, okay, let's make it, okay, let's say this calls outString, oops, so let's do outString is, okay, and we're going to do paste zero, set square brackets, oops, r$letters i, and then close bracket, oh, and actually, we need to append that to the, to the existing output string, otherwise we're going to overwrite it each time. So we actually need to do paste zero, outString, and then stick this stuff on the end. And, you know, once again, I'm going to say, like, this is not the fastest way to do these things. If you're, if you are making a string by just appending stuff to it, it actually, in R, it actually will allocate new memory for, it'll copy the whole string to a new location memory, and then put more, and append stuff to it, and that's not very efficient, but for this, since we're dealing with really small amounts of data, it's, it's totally fine, and it's, it's simple, and it's fast enough. Okay, so if it's correct, we do that, else if, let's copy and paste this, if it's, what did we say, inward, my else is messed up, okay, if it's inward, then we'll do the same thing, but we'll put parentheses around it, and else, we'll, instead of parentheses or brackets, we'll just use spaces. Okay, and then at the end, we'll return outString.

Okay, so here we have the result, let's check words, okay, when we want render text, not render print, reload, so I'll, wow, that's perfect, all right, and gives should be, okay, they're all correct there, great, hello, let's just try that one, great.