How to Code Wordle Into a Discord Bot

How to Code Wordle Into a Discord Bot Featured Image

Wordle is sweeping the nation like wild. In fact, it got so popular that the New York Times actually bought itOpens in a new tab.. With that said, I thought it would be fun to make my own version of Wordle as a Discord bot, so I wouldn’t have to wait to play once a day. Sound like a fun idea? Then, here’s your ticket to making your own!

Table of Contents

What Is Wordle?

You probably don’t need me to tell you what Wordle is at this point, but in case you’re reading this article in some far off future, let me take a minute to describe it. Wordle is a word game that has some pretty straightforward rules. The goal is to guess a 5-letter word that is provided by the computer.

As the player, we attempt to guess the 5-letter word without hints to start. Through each guess, we are given some pertinent information. For example, let’s imagine the word of the day is “coder.” We do not know this is the word of the day, so we guess “steal.” That’s my go-to guess! Since the guess contains some overlap with the actual word, we’re rewarded with the letter ‘e’ being highlighted in yellow:

steal

The color yellow tells us something very specific. The letter ‘e’ is in our target word, but it is not in the right place. In other words, we know the target word contains the letter ‘e’ but not in the third position.

From here, there are a lot of strategies for guessing a new word. One thing I like to do is guess another word with no overlapping letters with the first. For example, we might guess “bound” which is another one of my go-to guesses. In this case, we’ll be rewarded with the following colors:

bound

Suddenly, we know the target word contains ‘e’, ‘o’, and ‘d’. Not only that, but we know that ‘e’ is not in the third position, ‘d’ is not in the fifth position, and ‘o’ is in the second position (because it is highlighted in green). Because we know where ‘o’ goes, we also know that neither ‘e’ nor ‘d’ can go in the second position.

Now, let’s imagine that by some leap of faith we’re able to guess “coder” on the third try. Wordle will then reward use with all our letters in green! Even better, when we go to share our success with our peers, we’re given this nice graph of our experience in the form of a color grid as follows:

⬛⬛🟨⬛⬛
⬛🟩⬛⬛🟨
🟩🟩🟩🟩🟩

Of course, my goal today isn’t to tell you how to play Wordle. That’s something you probably already know how to do. Instead, you probably want to know how you can teach a Discord bot how to play this game. Let’s talk about that.

Building a Discord Bot

If you want to make a bot play Wordle, you need to first create a bot. This is one of the more annoying processes of Discord bot development. As a result, rather than rehash the countless tutorials out there, I’ll just share my favorite one: How to Make a Discord Bot in Python – Real PythonOpens in a new tab..

Of course, the short version goes something like this. Head on over to the Discord Developer PortalOpens in a new tab.. If you have an account, sign in. Otherwise, create one. Then follow the prompts for creating a bot. When successful, note down its token and invite the bot to your server.

While you’re getting everything ready, there is a bit of work you need to do on the coding side as well. Specifically, we’ll be using Python, so I recommend downloading the latest version. Feel free to make a more informed decision using this guide as well.

Once you have Python, you’ll need to install some libraries. For instance, in this tutorial, we’ll be using the discord.py libraryOpens in a new tab.. We’ll also be using slash commands, and there’s a handy library for that. Finally, to generate random words, we’ll be using the random word libraryOpens in a new tab.. To keep things simple, the following code should work to get everything you need:

pip install discord-py-slash-command
pip install Random-Word

Once you have your bot setup and the libraries above installed, you’re ready to write some code.

Coding the Wordle Bot

Every Discord bot starts with the same bit of code:

from discord.ext import commands

client = commands.Bot(
    command_prefix=commands.when_mentioned_or("!"),
)

client.run("Discord Bot Token")

Essentially, this is all the code we need to get a discord bot running. Just swap out the string in the last line with your Discord bot token, and you’ll have an official Discord bot.

Introducing Slash Commands

Of course, if we want the bot to do anything, we need to add some functionality. There are a lot of ways to do this, but I’m most fond of the relatively new slash commands.

Slash commands are like regular commands (e.g., !dosomething), but they have an entire Discord infrastructure built around them. As a result, once we’ve defined a slash command, our users will be able to use it without having know any special information. For instance, here’s what I see when I type a slash in my personal Discord server:

Discord Slash Command Menu Sample

You can personally add to this list by adding slash commands to your bot. There are a lot of ways to do this, but I like to take advantage of a third-party slash commands library. It allows us to do things like this:

@slash.slash(
    name="roll",
    description="A die roller",
    guild_ids=guild_ids,
    options=[
        create_option(
            name="maximum",
            description="The largest number on the die.",
            option_type=int,
            required=False
        )
    ]
)
async def _roll(ctx, maximum: int = 100):
    """
    Rolls a die.

    :param ctx: the context to send messages to
    :return: None
    """
    await ctx.send(f"Here's your roll mister: {random.randint(1, maximum)}")

The function defined above allows a user to generate a random integer of any size, defaulting to a max of 100. It probably looks a little messy, but the idea is straightforward. We want to create a function that performs some action. When stripped of comments and decorators, that function looks like this:

async def _roll(ctx, maximum: int = 100):
    await ctx.send(f"Here's your roll mister: {random.randint(1, maximum)}")

In other words, we have a function called `_roll`, and it has two inputs: the channel from which the slash command was executed and a maximum integer. To get this function working in Discord, we have to add all that junk above the function:

@slash.slash(
    name="roll",
    description="A die roller",
    guild_ids=[2314748104918],
    options=[
        create_option(
            name="maximum",
            description="The largest number on the die.",
            option_type=int,
            required=False
        )
    ]
)

This code is a decorator, and it’s responsible for setting up the rules around how the slash command can be used. For example, when you hover over the slash command in Discord, you’ll see the name and description. Likewise, the `guild_ids` specifies which Discord servers are allowed to use the slash command (FYI: I put a random number in that list. Make sure to copy your actual server ID or omit the line). Finally, the options field specifies options that you might like the user to select between, but we won’t be using this for Wordle.

Now, it’s important to note that we can’t actually use this slash command as-is. We’ll need to combine it with the code from above, along with some alterations. Namely, we need a new import as well as one additional line of code:

from discord.ext import commands
from discord_slash import SlashContext, SlashCommand

client = commands.Bot(
    command_prefix=commands.when_mentioned_or("!"),
)
slash = SlashCommand(client)

@slash.slash(
    name="roll",
    description="A die roller",
    guild_ids=guild_ids,
    options=[
        create_option(
            name="maximum",
            description="The largest number on the die.",
            option_type=int,
            required=False
        )
    ]
)
async def _roll(ctx, maximum: int = 100):
    """
    Rolls a die.

    :param ctx: the context to send messages to
    :return: None
    """
    await ctx.send(f"Here's your roll mister: {random.randint(1, maximum)}")

client.run("Discord Bot Token")

Without instantiating the slash object, we can’t actually make use of the slash commands. With that out of the way, let’s make a Wordle slash command.

Making a Wordle Slash Command

Given what we know about slash commands, we can start by creating a function called `_wordle`:

async def _wordle(ctx: SlashContext):
    pass

Then, to get the slash command working, we need to specify a couple fields in the decorator:

@slash.slash(
    name="wordle",
    description="Starts a game of wordle"
)
async def _wordle(ctx: SlashContext):
    pass

And that’s it! All that’s left is to embed this function in the code we created before:

from discord.ext import commands
from discord_slash import SlashContext, SlashCommand

client = commands.Bot(
    command_prefix=commands.when_mentioned_or("!"),
)
slash = SlashCommand(client)

@slash.slash(
    name="wordle",
    description="Starts a game of wordle"
)
async def _wordle(ctx: SlashContext):
    pass

client.run("Discord Bot Token")

If we hit run on this script, we should eventually see the slash command load into our Discord server (assuming you invited the bot to your server alreadyOpens in a new tab.). At the moment, the slash command will do nothing and likely cause an error. That said, we’re in a good place.

Generating a Random Word

The actual code for Wordle isn’t too bad. As long as we have a way of generating a random word, we should be able to create a game around it. Here’s what I’ve used to generate that random word:

import random_word

r = random_word.RandomWords()
new_word = r.get_random_word(
    hasDictionaryDef="true", 
    minLength=5, 
    maxLength=5
).lower()

Naturally, you can customize the types of words that are generated with this library. I personally chose to generate strictly 5-letter words that have dictionary definitions. I then convert whatever word I get to lowercase to make the comparisons easier.

As usual, we can take this code and put it in our shiny new Wordle function:

@slash.slash(
    name="wordle",
    description="Starts a game of wordle"
)
async def _wordle(ctx: SlashContext):
    r = random_word.RandomWords()
    new_word = r.get_random_word(
        hasDictionaryDef="true", 
        minLength=5, 
        maxLength=5
    ).lower()

Again, this code doesn’t do anything the user can see yet, but we’re getting there.

Adding Interaction

For a bot to be any good at its job, it needs to interact with the user. As a result, I think it’s good practice to signal to the user that the game is ready to begin. We do that by making use of the SlashContext object, `ctx`:

await ctx.send("Thanks for starting a game of Wordle. Make a guess!")

When running the slash command with a message like this, we should see a reply from the bot. Next, we’ll want some mechanism for accepting and responding to guesses. We can do this with a quick loop:

while guess != new_word:
    await ctx.send("Try again!")

For clarity, the code above is pseudocode. We’ll need to actually prompt for a guess. The way I’ve chosen to do this is to make use of the `wait_for` method which allows us to wait for user input based on some condition:

while (guess := await client.wait_for('message', check=check).content.lower()) != new_word:
    await ctx.send("Try again!")

Now, if you stare at that line of code for a bit, you might begin to have some questions. For example, what is the `:=` operator and why does the code say `check=check`. These are all valid questions that I’ll address in order.

First, that weird operator is called the walrus operator, and it’s stirred up a lot of controversy in the Python community. I quite like it for this exact scenario to eliminate a bit of duplicate code. It functions just like assignment, but we update the assignment at every iteration of the loop.

Second, the `check=check` line is how we add conditions by which to stop waiting. In this case, the second `check` refers to a function that I created to ensure that we only accept input from the same user in the same channel. It looks like this:

def check(m):
    return m.channel == ctx.channel and m.author == ctx.author

Now, when we put it all together, we get a function that looks like this:

@slash.slash(
    name="wordle",
    description="Starts a game of wordle"
)
async def _wordle(ctx: SlashContext):
    r = random_word.RandomWords()
    new_word = r.get_random_word(
        hasDictionaryDef="true", 
        minLength=5, 
        maxLength=5
    ).lower()
    await ctx.send("Thanks for starting a game of Wordle. Make a guess!")

    def check(m):
        return m.channel == ctx.channel and m.author == ctx.author

    while (guess := await client.wait_for('message', check=check).content.lower()) != new_word:
        await ctx.send("Try again!")

And if we were to run this, we’d have a game of Wordle without a lot of the nice features like knowing which letters are in the right place. Regardless, it’s playable!

Giving the Player Information

With the structure of the code in a good place, the next bit of code we need is a way of showing the user how close their guess is. To do that, I created a loop that iterates over the pairs of letters in both strings:

for expected, actual in zip(guess, new_word):
    pass

From here, we need to check three scenarios:

  1. Is the letter in the right spot?
  2. If not, is the letter in the word?
  3. If not, the letter is bad

Here’s what that looks like in code:

for expected, actual in zip(guess, new_word):
    if expected == actual:
        pass
    elif expected in new_word:
        pass
    else:
        pass

Once again, we have a framework for helping out the player. All that’s left is to decide on some form of messaging. To keep with Wordle tradition, I used the colored boxes and appended them to a string:

for expected, actual in zip(guess, new_word):
    line = ""
    if expected == actual:
        line += ":green_square:"
    elif expected in new_word:
        line += ":yellow_square:"
    else:
        line += ":black_large_square:"

And of course, to show the user how they’re doing, we need to share this line:

line = ""
for expected, actual in zip(guess, new_word):
    if expected == actual:
        line += ":green_square:"
    elif expected in new_word:
        line += ":yellow_square:"
    else:
        line += ":black_large_square:"
await ctx.send(line)

Again, incorporating all of this in the Wordle function will look as follows:

@slash.slash(
    name="wordle",
    description="Starts a game of wordle"
)
async def _wordle(ctx: SlashContext):
    r = random_word.RandomWords()
    new_word = r.get_random_word(
        hasDictionaryDef="true", 
        minLength=5, 
        maxLength=5
    ).lower()
    await ctx.send("Thanks for starting a game of Wordle. Make a guess!")

    def check(m):
        return m.channel == ctx.channel and m.author == ctx.author

    while (guess := await client.wait_for('message', check=check).content.lower()) != new_word:
        line = ""
        for expected, actual in zip(guess, new_word):
            if expected == actual:
                line += ":green_square:"
            elif expected in new_word:
                line += ":yellow_square:"
            else:
                line += ":black_large_square:"
        await ctx.send(line)

How’s this looking so far? I think it’s pretty good!

Showing the Final Grid

Perhaps the coolest and most satisfying part of Wordle is getting a grid to share at the end. Luckily, there’s not much to change. We need to modify our loop to store the entire grid, and we need to share that grid with the user. Here’s what that looks like:

@slash.slash(
    name="wordle",
    description="Starts a game of wordle"
)
async def _wordle(ctx: SlashContext):
    r = random_word.RandomWords()
    new_word = r.get_random_word(
        hasDictionaryDef="true", 
        minLength=5, 
        maxLength=5
    ).lower()
    await ctx.send("Thanks for starting a game of Wordle. Make a guess!")

    def check(m):
        return m.channel == ctx.channel and m.author == ctx.author

    grid = ""
    while (guess := await client.wait_for('message', check=check).content.lower()) != new_word:
        line = ""
        for expected, actual in zip(guess, new_word):
            if expected == actual:
                line += ":green_square:"
            elif expected in new_word:
                line += ":yellow_square:"
            else:
                line += ":black_large_square:"
        grid += f"{line}\n"
        await ctx.send(line)
    grid += ":green_square:" * 5
    
    await ctx.send(grid)

With just four more lines of code, we have a way of showing when the user has won!

Dealing With Words That Aren’t 5 Characters

One of the drawbacks or challenges of writing a Discord version of Wordle is that there is no way to constrain user input. For example, the user might provide a word that is shorter or longer than 5 characters which means they’ll waste a guess by default.

Fortunately, there’s an easy fix. Before looping, we can catch words that aren’t the right size:

@slash.slash(
    name="wordle",
    description="Starts a game of wordle"
)
async def _wordle(ctx: SlashContext):
    r = random_word.RandomWords()
    new_word = r.get_random_word(
        hasDictionaryDef="true", 
        minLength=5, 
        maxLength=5
    ).lower()
    await ctx.send("Thanks for starting a game of Wordle. Make a guess!")

    def check(m):
        return m.channel == ctx.channel and m.author == ctx.author

    grid = ""
    while (guess := await client.wait_for('message', check=check).content.lower()) != new_word:
        line = ""
        if len(guess) != 5:
            await ctx.send("Bad guess, mister! Try again.")
        else:
            for expected, actual in zip(guess, new_word):
                if expected == actual:
                    line += ":green_square:"
                elif expected in new_word:
                    line += ":yellow_square:"
                else:
                    line += ":black_large_square:"
            grid += f"{line}\n"
            await ctx.send(line)
    grid += ":green_square:" * 5
    
    await ctx.send(grid)

That said, what you’ll find is that dealing with these various edge cases can be somewhat messy. As a result, I recommend refactoring some of this code out into helper methods. For example, we might take the game logic and make it its own method. However, that’s sort of out of the scope of the tutorial.

Likewise, there are other things that need to be addressed like limiting the number of guesses to 6, dealing with non-alphabet characters, and dealing with duplicate letters. That said, I’m pleased with where this is at for now, and I think you can take it and adapt it to your needs.

Also, I’m planning to share a finalized version of this game over on PatreonOpens in a new tab. for folks who are into that sort of thing. In the meantime, let’s take a look at the tutorial script.

Putting It All Together

Given the novelty of Wordle at this point, I thought it would be fun to put together a Discord alternative. Here’s what that looks like in its final form:

from discord.ext import commands
from discord_slash import SlashContext, SlashCommand
import random_word

client = commands.Bot(
    command_prefix=commands.when_mentioned_or("!"),
)
slash = SlashCommand(client)

@slash.slash(
    name="wordle",
    description="Starts a game of wordle"
)
async def _wordle(ctx: SlashContext):
    r = random_word.RandomWords()
    new_word = r.get_random_word(
        hasDictionaryDef="true", 
        minLength=5, 
        maxLength=5
    ).lower()
    await ctx.send("Thanks for starting a game of Wordle. Make a guess!")

    def check(m):
        return m.channel == ctx.channel and m.author == ctx.author

    grid = ""
    while (guess := (await client.wait_for('message', check=check)).content.lower()) != new_word:
        line = ""
        if len(guess) != 5:
            await ctx.send("Bad guess, mister! Try again.")
        else:
            for expected, actual in zip(guess, new_word):
                if expected == actual:
                    line += ":green_square:"
                elif expected in new_word:
                    line += ":yellow_square:"
                else:
                    line += ":black_large_square:"
            grid += f"{line}\n"
            await ctx.send(line)
    grid += ":green_square:" * 5
    
    await ctx.send(grid)

client.run("Discord Bot Token")

As always, if you liked this sort of thing, feel free to let me know by giving this article a share. Also, if you are just learning Python and would like to get into Discord bots, I have a series for that! I also have a more traditional series for folks who want to learn Python the old school way. Similarly, here are some other Python articles that might interest you:

And if nothing else, I’d appreciate it if you took a moment to checkout my list of ways to grow the site. There, you’ll find out how the following resources (#ad) help the site:

Otherwise, take care and see you next time!

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Today, he pursues a PhD in Engineering Education in order to ultimately land a teaching gig. In his spare time, Jeremy enjoys spending time with his wife, playing Overwatch and Phantasy Star Online 2, practicing trombone, watching Penguins hockey, and traveling the world.

Recent Posts