diff --git a/sidebar.json b/sidebar.json
index 7683e342..2b84fd98 100644
--- a/sidebar.json
+++ b/sidebar.json
@@ -477,6 +477,10 @@
"name": "3D Rendering with Three.js",
"slug": "38-3D-rendering-with-threejs"
},
+ {
+ "name": "Wordle with Python and Rich",
+ "slug": "39-two-player-wordle-clone-python-rich"
+ },
{
"name": "Pinboard project part 1: HTML & CSS",
"slug": "pinboard-project-part-1"
diff --git a/static/images/tutorials/39-two-player-wordle-clone-python-rich/clearedinputs.png b/static/images/tutorials/39-two-player-wordle-clone-python-rich/clearedinputs.png
new file mode 100644
index 00000000..c766fed9
Binary files /dev/null and b/static/images/tutorials/39-two-player-wordle-clone-python-rich/clearedinputs.png differ
diff --git a/static/images/tutorials/39-two-player-wordle-clone-python-rich/feedback.png b/static/images/tutorials/39-two-player-wordle-clone-python-rich/feedback.png
new file mode 100644
index 00000000..3681bf45
Binary files /dev/null and b/static/images/tutorials/39-two-player-wordle-clone-python-rich/feedback.png differ
diff --git a/static/images/tutorials/39-two-player-wordle-clone-python-rich/gameloop.png b/static/images/tutorials/39-two-player-wordle-clone-python-rich/gameloop.png
new file mode 100644
index 00000000..b85eee8e
Binary files /dev/null and b/static/images/tutorials/39-two-player-wordle-clone-python-rich/gameloop.png differ
diff --git a/static/images/tutorials/39-two-player-wordle-clone-python-rich/new-repl.png b/static/images/tutorials/39-two-player-wordle-clone-python-rich/new-repl.png
new file mode 100644
index 00000000..ab5719d1
Binary files /dev/null and b/static/images/tutorials/39-two-player-wordle-clone-python-rich/new-repl.png differ
diff --git a/static/images/tutorials/39-two-player-wordle-clone-python-rich/shell.png b/static/images/tutorials/39-two-player-wordle-clone-python-rich/shell.png
new file mode 100644
index 00000000..6c17529f
Binary files /dev/null and b/static/images/tutorials/39-two-player-wordle-clone-python-rich/shell.png differ
diff --git a/static/images/tutorials/39-two-player-wordle-clone-python-rich/twordledemo.mp4 b/static/images/tutorials/39-two-player-wordle-clone-python-rich/twordledemo.mp4
new file mode 100644
index 00000000..c5d9e2ac
Binary files /dev/null and b/static/images/tutorials/39-two-player-wordle-clone-python-rich/twordledemo.mp4 differ
diff --git a/static/images/tutorials/39-two-player-wordle-clone-python-rich/welcometowordle.png b/static/images/tutorials/39-two-player-wordle-clone-python-rich/welcometowordle.png
new file mode 100644
index 00000000..1002f34c
Binary files /dev/null and b/static/images/tutorials/39-two-player-wordle-clone-python-rich/welcometowordle.png differ
diff --git a/static/images/tutorials/39-two-player-wordle-clone-python-rich/withemoji.png b/static/images/tutorials/39-two-player-wordle-clone-python-rich/withemoji.png
new file mode 100644
index 00000000..9a93a775
Binary files /dev/null and b/static/images/tutorials/39-two-player-wordle-clone-python-rich/withemoji.png differ
diff --git a/tutorials/39-two-player-wordle-clone-python-rich.md b/tutorials/39-two-player-wordle-clone-python-rich.md
new file mode 100644
index 00000000..c606cf9e
--- /dev/null
+++ b/tutorials/39-two-player-wordle-clone-python-rich.md
@@ -0,0 +1,326 @@
+# Building a two player *Wordle* clone with Python and Rich on Replit
+
+In this guide, we'll build a version of the popular game *Wordle*. Instead of the computer providing a word that the player has to guess, our version will work with two players. Player 1 will enter the word, and then player 2 will try to guess the word entered, similar to the popular game *Hangman*.
+
+Once you're done, you'll be able to play a command-line-based game with a friend (with both of you sitting at the same machine), as shown below.
+
+
+
+
+We'll be using Python, and to do the green and yellow colors we'll use [Rich](https://rich.readthedocs.io/en/stable/introduction.html), a library for rich-text formatting. To follow along, you should know some basic Python, but we'll explain each code sample in depth so you should be able to keep up even if you are not familiar with Python.
+
+## Getting started
+
+To get started, create a **Python** repl.
+
+
+
+
+## Installing Rich
+
+Rich isn't part of the Replit Universal Installer, so we have to install it manually. Open up the "Shell" tab in the repl workspace and run the following commands:
+
+```bash
+python3 -m poetry init --no-interaction
+python3 -m poetry add rich
+```
+
+This will create a `pyproject.toml` file to define Rich as a dependency, and Replit will automatically install it for us next time we run our app.
+
+
+
+## Printing colored text
+
+The first thing we need to figure out is how to print out different colored letters. By default, we'll use similar settings to the *Wordle* defaults
+
+* Green = correct letter in the correct position
+* Yellow = correct letter in the incorrect position
+* Gray = incorrect letter
+
+Because we're using Rich, we don't have to mess around with [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). It's possible to use them to style terminal text, but you end up having to deal with nasty-looking strings like `\033[0;32m`, and there are likely to be compatibility issues too. Rich abstracts this away for us, and we can use nicer-looking controls like '[black on green]TWORDLE[/]' to describe how the text should look.
+
+Take a look at how this works now by adding the following code to `main.py` and pressing "Run":
+
+```python
+import rich
+
+rich.print('[black on green]TWORDLE[/]')
+```
+
+Because we may want to customize what specific colors mean at some point, let's define each of our three cases in functions. Replace the existing code in `main.py` with the following:
+
+```python
+import rich
+
+def correct_place(letter):
+ return f'[black on green]{letter}[/]'
+
+def correct_letter(letter):
+ return f'[black on yellow]{letter}[/]'
+
+def incorrect(letter):
+ return f'[black on white]{letter}[/]'
+
+WELCOME_MESSAGE = correct_place("WELCOME") + " " + incorrect("TO") + " " + correct_letter("TWORDLE") + "\n"
+
+def main():
+ rich.print(WELCOME_MESSAGE)
+
+if __name__ == '__main__':
+ main()
+```
+
+Run this code, and you'll see a Wordle-styled welcome message, demonstrating all three styles, as shown below.
+
+
+
+
+## Creating the game loop
+
+As in classic *Wordle*, our game will allow the player six tries to guess a word. Unlike classic *Wordle*, we'll allow for two players. Player 1 will choose a word, and player 2 will attempt to guess it. The basic logic is then:
+
+```
+Get word from Player 1
+Get guess from Player 2
+While Player 2 has guesses remaining
+ Get new guess
+ If guess is correct
+ End the game
+```
+
+So let's ignore our fancy colored text for a moment and build this logic.
+
+### Getting and guessing the word
+
+We'll use the `Console` class from Rich, which creates a virtual output pane on top of our actual console. This will make it easier to have more control over our output as we build out the app.
+
+Add the following two imports to the top of the `main.py` file:
+
+```python
+from rich.prompt import Prompt
+from rich.console import Console
+```
+
+And now replace the `main()` function with the following code:
+
+```python
+def main():
+ rich.print(WELCOME_MESSAGE)
+
+ allowed_guesses = 6
+ used_guesses = 0
+
+ console = Console()
+ answer_word = Prompt.ask("Enter a word")
+ console.clear()
+
+ while used_guesses < allowed_guesses:
+ used_guesses += 1
+ guess = Prompt.ask("Enter your guess")
+ if guess == answer_word:
+ break
+ print(f"\n\nTWORDLE {used_guesses}/{allowed_guesses}\n")
+```
+
+If you run this, you'll be prompted (as player 1) to enter a word. The entered word will then be hidden from view to avoid spoiling the game, and player 2 can enter up to six guesses. At this stage, player 2 doesn't get any feedback on correct or incorrect letters, which makes the game pretty hard for player 2! If player 2 does happen to guess correctly, the loop will break and the game will display how many guesses were used.
+
+
+
+
+
+### Providing feedback on correct letters
+
+Let's add a helper function to calculate whether each letter should be green, yellow, or gray. Add this function above the `main()` function in `main.py`:
+
+```python
+def score_guess(guess, answer):
+ scored = []
+ for i, letter in enumerate(guess):
+ if answer[i] == guess[i]:
+ scored += correct_place(letter)
+ elif letter in answer:
+ scored += correct_letter(letter)
+ else:
+ scored += incorrect(letter)
+ return ''.join(scored)
+```
+
+This function takes in player 2's guess and the correct answer and compares them letter by letter. It uses the helper functions we defined earlier to create the Rich formatting string for each letter, and then joins them all together into a single string.
+
+
+
+
+## Adding an emoji representation for spoiler-free sharing
+
+A key part of *Wordle* is that once a player has guessed a word, they can share a simple graphic of how well they did, without giving away the actual word. For our two-player version, this "no spoilers" feature isn't as important, but let's add it anyway.
+
+As with the letter-coloring, we want to keep the emoji we use configurable. By default, we'll use green, yellow, and gray squares. Let's start by defining this in a dictionary, near the top of our `main.py` file. Add the following to your code:
+
+```python
+emojis = {
+ 'correct_place': '🟩',
+ 'correct_letter': '🟨',
+ 'incorrect': '⬜'
+}
+```
+
+Replace the `score_guess` function with the following:
+
+```python
+def score_guess(guess, answer):
+ scored = []
+ emojied = []
+ for i, letter in enumerate(guess):
+ if answer[i] == guess[i]:
+ scored += correct_place(letter)
+ emojied.append(emojis['correct_place'])
+ elif letter in answer:
+ scored += correct_letter(letter)
+ emojied.append(emojis['correct_letter'])
+ else:
+ scored += incorrect(letter)
+ emojied.append(emojis['incorrect'])
+ return ''.join(scored), ''.join(emojied)
+```
+
+The logic is very similar to before, but instead of only calculating the correct style for the letter, we also keep track of each emoji. At the end, we return both the string to print out the scored word, and the emoji representation for that guess.
+
+To use this in the main function, replace the code for the `while` loop with the following code:
+
+```python
+ all_emojied = []
+ while used_guesses < allowed_guesses:
+ used_guesses += 1
+ guess = Prompt.ask("Enter your guess")
+ scored, emojied = score_guess(guess, answer_word)
+ all_emojied.append(emojied)
+ console.print(scored)
+ if guess == answer_word:
+ break
+ print(f"\n\nTWORDLE {used_guesses}/{allowed_guesses}\n")
+ for em in all_emojied:
+ console.print(em)
+```
+
+If you run again, the game will work as before, but now you'll see the emoji representation printed after the game ends. This can be copy-pasted to share and help our game go viral. You can see what it looks like in the image below.
+
+
+
+## Some finishing touches
+
+The one messy part of our game remaining is that the input prompts are still shown after player 2 has entered each guess. This means that each word is shown twice: once in its colored form, and once exactly as the player entered it. Let's adapt the game to clear the console and output just the colored versions of each guess.
+
+To do this, we need to keep track of all player 2's guess, which we were not doing before.
+
+Replace the `while` loop in the `main()` function with the following code:
+
+```python
+ all_emojied = []
+ all_scored = []
+ while used_guesses < allowed_guesses:
+ used_guesses += 1
+ guess = Prompt.ask("Enter your guess")
+ scored, emojied = score_guess(guess, answer_word)
+ all_scored.append(scored)
+ all_emojied.append(emojied)
+ console.clear()
+ for scored in all_scored:
+ console.print(scored)
+ if guess == answer_word:
+ break
+```
+
+This clears the console completely after each guess by player 2, and then prints out each of the (styled) guesses. The output looks neater now, as shown below.
+
+
+
+### Adding instructions
+
+People will like our game more if they can figure out what to do without having to read documentation. Let's add some basic instructions for each player to the game interface. Below the `WELCOME_MESSAGE` variable we defined earlier, add the following:
+
+```python
+P1_INSTRUCTIONS = "Player 1: Please enter a word (player 2, look away)\n"
+P2_INSTRUCTIONS = "Player 2: You may start guessing\n"
+```
+
+Now update the `main()` function like this:
+
+```python
+def main():
+ allowed_guesses = 6
+ used_guesses = 0
+
+ console = Console()
+ console.print(WELCOME_MESSAGE)
+ console.print(P1_INSTRUCTIONS)
+ answer_word = Prompt.ask("Enter a word")
+ console.clear()
+ console.print(WELCOME_MESSAGE)
+ console.print(P2_INSTRUCTIONS)
+
+ all_emojied = []
+ all_scored = []
+ while used_guesses < allowed_guesses:
+ used_guesses += 1
+ guess = Prompt.ask("Enter your guess")
+ scored, emojied = score_guess(guess, answer_word)
+ all_scored.append(scored)
+ all_emojied.append(emojied)
+ console.clear()
+ console.print(WELCOME_MESSAGE)
+ for scored in all_scored:
+ console.print(scored)
+ if guess == answer_word:
+ break
+ print(f"\n\nTWORDLE {used_guesses}/{allowed_guesses}\n")
+ for em in all_emojied:
+ console.print(em)
+```
+Now our welcome message stays at the top, and the players are prompted by simple instructions. Have fun playing it with your friends!
+
+## Where next?
+
+The basics of the game are in place, but there is still a lot you could build from here. Some ideas:
+
+* Fix the logic for handling duplicate letters.
+* Fix the fact that the game crashes if player 2 enters the wrong number of letters.
+* The game still says `6/6`, even if player 2 has not guessed the word after six tries. Have the game print out `X/6` in this case, as in classic *Wordle*.
+* Give player 2 more guesses based on the length of the word player 1 enters.
+* [CHALLENGING] Make the game work over the internet instead of requiring both players to be in same room.
+
+
+You can find the code for this tutorial here:
+
+