Play / Practice Wordle on ESP32

11K1729

Intro: Play / Practice Wordle on ESP32

Have you ever seen this matrix of letters, with green - yellow - grey cells? It is getting viral, with tons of tweets about it...

The game is called WORDLE, you can play it online here. It's an online word game, with a new puzzle every day. You have 6 chances to find a five-letter word, in English. After your guess, the color of each letter indicates whether it is well placed (green), it exists but in another spot (yellow) or it is not in the word (grey). You can see the rules up there.

Wordle is super popular: over 300,000 people play it daily, according to The New York Times.

This game can become quite addictive, but you only have one game each day... Everyone is playing the exact same puzzle, so you can share your score (on Twitter for example) with your friends and discuss online about it.

Wordle was created by Josh Wardle, an engineer living in Brooklyn. You can find lots of tips and tricks on how to improve your score, but the best way to win is to practice. This is why I created a version on an ESP32.

STEP 1: Prepare the Game

First you need to find a dictionary, a list of 5-letter words. It is required both to choose the target word, and to verify that your guesses are correct.

I found a few of them online, in English and in French:

  • English
  • https://www-cs-faculty.stanford.edu/~knuth/sgb-words.txt (5757 words)
  • https://www.bestwordlist.com/5letterwords.htm (12478 words)
  • French:
  • https://fr.wikwik.org/mots5lettres.htm (23595 words)
  • https://www.listesdemots.net/mots5lettres.htm (7980 words)

The average vocabulary of native-english speakers is between 40,000 and 50,000 words. But interestingly, there exists an active and passive vocabulary. An active vocabulary consists of the words we have learned and actually use when speaking or writing. On the other hand, a passive vocabulary refers to words we’ve assimilated but do not really use. The average active vocabulary of an adult English speaker is around 20,000 words, while his passive vocabulary is around 40,000 words.

So, if you consider the first list of 5757 words, knowing that they are all 5-letter words, there is a great chance that most of the standard 5-letter words of the average active vocabulary exist in this list. But if you want a tougher game, you can use the longer list...

If you want to create similar lists in another language, you just need to find them online and create a txt file with all the words separated by a space. I uploaded 3 files below: English (long and short) and French (short).

If you want a more complicated game, with more than 5 letters (or less), first create the associated dictionary (here is a list of 4-letter words, and here for 6). Then edit the ino file and change this line:

#define NUMBER 5 // Number of letters (don't change unless you can change the dictionary)

You may also want more than 6 trials: just change this line (near the end of the ino file):

    if (++trials == NUMBER + 1) {

For example, for 10 trials:

    if (++trials == 10) {


To play the game, I assume you already know how to use an ESP32 with the Arduino IDE, how to compile a C++ code and upload it on the microcontroller. If not, please refer to this page, for example.

In your Arduino folder, create a new folder called 'Wordle'. In this folder, create another one called 'data'. Put the file Wordle.ino in the Wordle folder, and the txt files in the data folder.

This will look like this:

  • Wordle
  • Wordle.ino
  • data
  • Dict-5757.txt
  • Dict-12478.txt
  • Dict-FR-7980.txt

Of course, you can only use the dictionary files you want. But they are quite light and do not really impair the memory of the ESP32. To upload the txt files on your ESP32, you must use the ESP32 Sketch Data Uploader. Everything is explained here. Remember that you must close the serial monitor to upload the files.

You only need to upload them once.

By default, the game assumes that you use the Dict-5757 file (English words). If you want to change this, edit the ino file and change this line:

const char wordFile[] = "/Dict-5757.txt";

Each time you guess a word, the program searches it in the dictionary file. With this file, it takes up to 450ms to verify that the word is in the dictionary. If you want to use the longer file (Dict-12478.txt), you just need to change the name in the line above, but it may take up to 2 seconds to verify your entry.

If you don't want to wait, or if you want to try random words or words that don't exist, just change this line:

#define CHECK true // set false if you don't want to check your input is in the dict

Change 'true' for 'false'. The program will not check your guess in the dictionary.


Run the program. After a reminder of the rules, you can enter your first guess. All character that are not letters are ignored. If you enter more than 5 letters, only the first five are accepted. If you enter less than 5 letters, the program waits until you have entered all 5 letters.

Then the result is displayed on the monitor: below your word are a series of signs. A '+' means that the letter above is well placed. A '=' means that it is present in the word but not at this place. And a '-' means that the letter is not in the word.

If you input a '?' (without quotes), the program gives you the information you have so far: the list of well placed letters and the letters you tried but that are not in the word.


Here is an example:

Please enter a 5 letter word.
ABOUT
=----
BLIPS
----=
THERE
-+-+-
Here is what you know so far...
Well placed: -H-R-
Not in the Wordle: BEILOPTU
CHARS
-+++=
SHARE
++++-
SHARK
+++++
Congratulations, you found it in 6 trials!!

The word was SHARK. You can see that my first guess was ABOUT and only the 'A' is present, but not at the first place. Later, the word THERE shows that the H and the R are well placed (second and fourth positions).

? summarizes where H and R are placed, and shows that B E I L O P T U are not present in the word.

SHARE was a good guess, which led me to SHARK, in 6 trials.

STEP 2: Graphical Version

I also wrote a graphical version of the game, which mimics the original interface on the TT-GO T-display. To use it, you need the files below. Create in your Arduino folder a new folder called 'Wordle_TTGO', and do the same as explained above (data folder, dictionary files, upload, etc) with the additional 'Free_Fonts.h' file (which goes in the 'Wordle_TTGO' folder).

You also need to install, if not already done, the TFT_eSPI library, using the library manager of the Arduino IDE. Then you have to do the following:

Select the driver for the TT-GO: in the file C:\Users\xxxxxx\Documents\Arduino\libraries\TFT_eSPI\User_Setup_Select.h :

  • comment the line #include <User_Setup.h>
  • uncomment the line #include <User_Setups/Setup25_TTGO_T_Display.h>

The rest is similar to the previous step.


Here is another game, that goes with the photos:

Please enter a 5 letter word.
ABOUT
---=-
THERE
-----
LIVES
-=--+
Here is what you know so far...
Well placed: ----S
Not in the Wordle: ABEHLORTV
QUITS
+++-+
AZERT
This word is not in my dictionary, try again
QUIPS
+++++
Congratulations, you found it in 5 trials!!


Enjoy!

STEP 3: How to Win?

If you want to win, read my other instructables "Win at Wordle"

20 Comments

Hello, I have hard time reading French. Is there updated code for TT-GO to use encoder?
Warning: you also need a data directory (dictionaries) and the 'Free_Fonts.h' file for everything to work

Please send me your email to noone38@gmail.com
I will send you the complete file
No, I didn't do it, because I don't have the encoder. But maybe "NoOne38" did it. He wanted to adapt it for his grandson.
Am I correct, These only work with a pc connected?
Yes, because you need the keyboard to enter your word.
Bonsoir Fabrice
Mille mercis pour la patience ....et pour le test
j'ai procédé à la modification
je bloque sur :
==> 'letter' was not declared in this scope <==
il semblerai que la déclaration ne soit pas adaptée
j'ai donc essayé çà :
==> char ( '@'+letter)
mais sans succès

je continue ....
pascal
Oui, je vois. 'letter' est une variable locale, connue seulement dans le bloc entre accolades où elle est déclarée. Mais je l'utilise ensuite plus loin, c'est pas bon.
Il faut déclarer letter plus haut, par exemple après 'int d=5;' :
char letter;
puis ôter 'char' dans 'char letter = rotaryEncoder.readEncoder();'
Super Fabrice ! çà fonctionne mieux encore que je ne l’espérais
mille mercis
il faut que j'y apporte quelques corrections :
1) il faut que je trouve le moyen d'effacer le mot dans :
void dispNoDict() { => après le message
2) de diminuer la taille des messages pour qu'ils n'occupent que les 2 dernières lignes ( après la 6ième ligne en fait )
3) une question : à quoi servent BUTTON_1 et BUTTON_2 stp ?

Encore un énorme merci
pascal

PS:
Pour essai j'ai voulu essayer d'introduire le nombre de lettre possible à savoir 3,4ou5
pour ce faire j'ai fait le fichier TEST ci-dessous :
****************************************************
#define CHECK true // mettez false si vous ne voulez pas vérifier que votre entrée est dans le dict.
//#define NUMBER 5 // Nombre de lettres (ne pas changer sauf si vous pouvez changer le dictionnaire)

#include "FS.h"
#include "SPIFFS.h"
#define FORMAT_SPIFFS_IF_FAILED true
#include <TFT_eSPI.h>
#include <SPI.h>
#include "Free_Fonts.h"
#include <AiEsp32RotaryEncoder.h>

#define NUMBER
#define TFT_WIDTH 135
#define TFT_HEIGHT 240
#define BUTTON_1 35
#define BUTTON_2 0
TFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT);

const char wordFile[] = "";


#define ROTARY_ENCODER_A_PIN 33
#define ROTARY_ENCODER_B_PIN 32
#define ROTARY_ENCODER_BUTTON_PIN 25
#define ROTARY_ENCODER_STEPS 4
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS);
void IRAM_ATTR readEncoderISR()
{
rotaryEncoder.readEncoder_ISR();
}

void initTFT() {
// Prepare the display
tft.begin();
tft.fillScreen (TFT_BLACK);
tft.setRotation(0);
}
void initNBLetter()
{
tft.setTextDatum(TC_DATUM);
tft.setFreeFont(FSSB12);
int h = 20;
int d = 5;
char letter;
while (not rotaryEncoder.isEncoderButtonClicked()) {
if (rotaryEncoder.encoderChanged()) {
letter = rotaryEncoder.readEncoder();
// display letter
tft.fillRect(0, TFT_HEIGHT - 63, TFT_WIDTH, 63, TFT_BLACK);
int y = (h + 3 * d);
int x = (h + d) + 3;
tft.fillRect(x, y, h + d, h + d, TFT_ORANGE);
tft.drawRect(x, y, h + d, h + d, TFT_BLUE);
tft.setCursor(x + h / 4, y + h);
tft.setTextColor(TFT_WHITE);
tft.print(letter);
}
}

if (letter == 51) {
NUMBER=3; <=============== erreur
const char wordFile[] = "fichier3.txt";
}
if (letter == 52) {
NUMBER=4;
const char wordFile[] = "fichier4.txt";
}
if (letter == 53) {
NUMBER=5;
const char wordFile[] = "fichier5.txt";
}
}

//*************************************************
// SETUP
//*************************************************

void setup ()
{

//***********************************************
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(51, 53, true);
//***********************************************
Serial.begin (115200);
initTFT();
if (!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) {
Serial.println("Échec du montage du SPIFFS");
return;
}
// valider le nb de lettres
initNBLetter(); //<===========================
// Ouvrir le fichier du dictionnaire
File file = SPIFFS.open(wordFile);
if (!file || file.isDirectory()) {
Serial.printf("%s: Impossible d'ouvrir le fichier pour la lecture\n", wordFile);
// while (1);
}

}

//*************************************************
// LOOP
//*************************************************

void loop() {

}

***********************************************************************
mais j'ai une erreur à la compilation
ligne : >>NUMBER=5;
expected primary-expression before '=' token


Bonne nouvelle. J'espère que ton petit fils sera content.
Les boutons ne sont pas utilisés.
Pour l'erreur, c'est à cause du #define. C'est une directive du compilateur qui dit que chaque fois qu'il voit le mot NUMBER il le remplace par ce qui suit, 5 dans mon cas. Pour toi il faut changer cette ligne par
int NUMBER;
Et ça devrait compiler
Merci Fabrice
si çà ne t'ennuies pas je continue à te donner les nouvelles de l'évolution
pascal
Bonjour Fabrice
encore merci de vos réponses

je suis confus de vous importuner une fois de plus mais pouvez-vous me donner quelques précisons sur le prg svp ?
1) pourquoi avoir utiliser [NUMBER+1]
par exemple avec le nombre d'essais qui doit être de 6 quelque soit le nb de lettres et (NUMBER+1) je pense
j'ai mis
if (++trials == 6) {
à la place de
if (++trials == NUMBER + 1) {

2)
pour le test des mots dans le dictionnaire j'ai du modifier des lignes
de prg car il ne trouvait pas les correspondances des mots pour 3 ou 4
lettres mais seulment pour 5 lettres j'ai mis :
file.seek((NUMBER + 1) * i, SeekSet);
à la place de
file.seek(6 * i, SeekSet);


En ce qui concerne le choix du nombre de lettres , j'ai du renoncer car trop compliqué à gérer avec le terme :
byte check[NUMBER + 1] = {0};
qui refusait la compilation

merci encore
pascal
Bonsoir Pascal. Je pense qu'il vaudrait mieux continuer par mail (lesept777@gmail.com), ou sur le forum Arduino (https://forum.arduino.cc/c/international/francais/....
Les tableaux de char sont particuliers, car il faut qu'ils se terminent par un caractère nul pour indiquer au µC la fin du tableau. Pour stocker 5 lettres, on déclare donc un tableau de 6 cases, d'où NUMBER+1.
Pour le file.seek : bien vu, j'avais oublié de changer le 6.
Pour le choix du nombre de lettres, on y arrivera. Le mieux est de créer une fonction qui sera appelée au début du setup.
Si tu parles de la ligne :
if (++trials == NUMBER + 1)
l'instruction '++trials' incrémente la valeur de trials AVANT de faire le test. il faut donc comparer avec NUMBER + 1. Je pense (à vérifier) que si j'avais écrit trials++, il aurait fallu comparer avec NUMBER car l'incrémentation aurait été faire APRES le test.
Bonjour
Bravo, très intéressé par votre projet mais je souhaiterai entrer le mot à chercher à l'aide d'un Encodeur rotatif type KY-040 plutôt que d'utiliser la console
pensez-vous que cela soit possible et si oui pouvez-vous m'aider svp ?
merci
Bonjour et merci. Le mot à chercher est choisi par le programme au hasard. Vous voulez dire "entrer le mot proposé" ?
Bonjour ,
merci d'avoir répondu
je me suis mal exprimé pardon : il s'agit en fait du mot que l'on propose via la console actuellement
en fait, je désire faire ce projet pour mon petit-fils (5 ans) qui va apprendre à lire, il est évident que de passer par la console rend le projet plus compliqué, ma proposition est de passer par un KY-40 pour qu'il puisse choisir les lettres du mot proposé
1 appui court valide la lettre puis passage à la lettre suivante et ce jusqu'à 5
puis 1 appui long pour valider le mot proposé
OK, vous savez programmer sur ESP32 ou Arduino, ou pas du tout ?
Bonjour,
Il serait prétentieux pour ma part de répondre OUI mais je me débrouille , j'avais essayé de faire ce prog test :
************************************************
#define PIN_A 32
#define PIN_B 33
#define PIN_BUTTON 23

#define DEBONCE_TO 200

volatile bool turnedCW = false;
volatile bool turnedCCW = false;

unsigned long debounceTime = 0;

bool lastWasCW = false;
bool lastWasCCW = false;

void checkEncoder() {
if ((!turnedCW)&&(!turnedCCW)) {
int pinA = digitalRead(PIN_A);
delayMicroseconds(2000);
int pinB = digitalRead(PIN_B);
if (pinA == pinB){
if (lastWasCW) {
turnedCW = true;
} else {
turnedCCW = true;
}
} else {
if (lastWasCCW) {
turnedCCW = true;
} else {
turnedCW = true;
}
}
}
}

void setup() {
Serial.begin(115200);
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_BUTTON, INPUT_PULLUP);
attachInterrupt(PIN_B, checkEncoder, RISING);
Serial.println("Lecture de l'encodeur : ");
}

void loop() {
static int value = 1;

if (!digitalRead(PIN_BUTTON)) {
//value = 0;
Serial.print("Reset: ");
Serial.println(char('@'+value));
}

if (turnedCW) {
value++;
if (value>26){
(value = 1);
}
Serial.print("Droite : ");
//Serial.println(value);
Serial.println(char('@'+value));
turnedCW = false;
lastWasCW = true;
debounceTime = millis();
}

if (turnedCCW) {
value--;
if (value<1){
(value = 26);
}
Serial.print("Gauche : ");
//Serial.println(value);
Serial.println(char('@'+value));
turnedCCW = false;
lastWasCCW = true;
debounceTime = millis();
}

if ((millis()-debounceTime) > DEBONCE_TO) {
lastWasCW = false;
lastWasCCW = false;
}
}
******************************************
d'inscrire une lettre de A à Z mais je m'étais arrêté là car ne sachant pas comment programmer le décalage de gauche à droite ( pour les 5 lettres ) et pour valider le mot choisi

pascal



Bonjour Pascal. Je pense qu'il est plus simple d'utiliser une bibliothèque pour faire la surveillance de l'encodeur. Je n'ai jamais joué avec le KY-040 et je n'en ai pas, donc je ne peux pas tester, mais j'ai trouvé la bibliothèque suivante : https://github.com/igorantolic/ai-esp32-rotary-enc...
L'exemple https://github.com/igorantolic/ai-esp32-rotary-enc... est tout simple : on définit les paramètres de l'encodeur et ensuite il suffit de tester si sa valeur a changé :
if (rotaryEncoder.encoderChanged())
{
Serial.println(rotaryEncoder.readEncoder());
}
Et c'est pareil avec le bouton.
On peut paramétrer les valeurs renvoyées par l'encodeur dans un intervalle cyclique : par exemple
rotaryEncoder.setBoundaries(65, 90, true); //minValue, maxValue, circleValues true|false (when max go to min and vice versa)
65 et 90 correspondent aux codes ASCII de A et Z. On obtient la lettre en mettant cette valeur dans un char.
Donc, il faut modifier la fonction readWord de mon code pour lui faire lire l'encodeur et son bouton, et afficher les lettres sélectionnées au bon endroit. Je pense que c'est tout...
Fabrice
Voila une proposition de modification :

bool readWord(int line) {
byte n = 0;
tft.setTextDatum(TC_DATUM);
tft.setFreeFont(FSSB12);
int h = 20;
int d = 5;

while (n < NUMBER) {
while (not rotaryEncoder.isEncoderButtonClicked()) {
if (rotaryEncoder.encoderChanged()) {
char letter = rotaryEncoder.readEncoder();
// display letter
tft.fillRect(0, TFT_HEIGHT - 63, TFT_WIDTH, 63, TFT_BLACK);
int y = line * (h + 3 * d);
int x = n * (h + d) + 3;
tft.fillRect(x, y, h + d, h + d, TFT_ORANGE);
tft.drawRect(x, y, h + d, h + d, TFT_BLUE);
tft.setCursor(x + h / 4, y + h);
tft.setTextColor(TFT_WHITE);
tft.print(letter);
}
}
Word[n++] = letter;
}
Serial.println(Word);
return true;
}


Il faut modifier l'appel de la fonction comme suit :
if (readWord(trials)) {

Et bien sûr configurer l'encodeur comme dans l'exemple :
#define ROTARY_ENCODER_A_PIN 32
#define ROTARY_ENCODER_B_PIN 21
#define ROTARY_ENCODER_BUTTON_PIN 25
#define ROTARY_ENCODER_STEPS 4
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS);
void IRAM_ATTR readEncoderISR()
{
rotaryEncoder.readEncoder_ISR();
}

et dans le setup :
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(65, 90, true);

A tester...