Rucksikaa Raajkumar
Published © MIT

Lifeline Letters - Friendly Hangman Game with M5Stack Core2

A friendly version of Hangman game (a word game) using handwritten letters recognized with the help of TinyML techniques.

IntermediateFull instructions provided8 hours26
Lifeline Letters - Friendly Hangman Game with M5Stack Core2

Things used in this project

Hardware components

M5Stack Core2 ESP32 IoT Development Kit
M5Stack Core2 ESP32 IoT Development Kit
×1

Software apps and online services

Neuton
Neuton Tiny ML Neuton
Arduino IDE
Arduino IDE
Python IDLE

Story

Read more

Code

Lifeline Letters - Python

Python
This is the Python code for this project
from tkinter import *
import tkinter.font as font
from tkinter import messagebox
import random
from PIL import Image, ImageTk
import serial
import time

# Word list with the corresponding hints
word_list = {
    "easy": [
        {"word": "cow", "hint": "A farm animal that gives milk."},
        {"word": "lip", "hint": "Part of your mouth."},
        {"word": "map", "hint": "Shows directions and places."},
        {"word": "ram", "hint": "A male sheep."},
        {"word": "cap", "hint": "Something you wear on your head."},
        {"word": "pig", "hint": "A pink farm animal that oinks."},
        {"word": "boy", "hint": "A young male child."},
        {"word": "oil", "hint": "Used for cooking or fuel."},
        {"word": "bag", "hint": "You carry things in it."},
        {"word": "wig", "hint": "Fake hair that you can wear."}
    ],
    "medium": [
        {"word": "royal", "hint": "Belonging to a king or queen."},
        {"word": "solar", "hint": "Related to the sun."},
        {"word": "grass", "hint": "Green plants that cover the ground."},
        {"word": "loyal", "hint": "Faithful and true to someone."},
        {"word": "polar", "hint": "Opposite ends of the Earth."},
        {"word": "crisp", "hint": "Crunchy and fresh."},
        {"word": "gorilla", "hint": "A large ape."},
        {"word": "local", "hint": "Nearby or from your area."},
        {"word": "glass", "hint": "A clear material for windows."},
        {"word": "roams", "hint": "Moves around without a fixed path."}
    ],
    "hard": [
        {"word": "gossip", "hint": "Talking about other people."},
        {"word": "cowboy", "hint": "A person who herds cattle."},
        {"word": "willow", "hint": "A type of tree with long, drooping branches."},
        {"word": "payroll", "hint": "A list of employees to be paid."},
        {"word": "safari", "hint": "A trip to see wild animals in Africa."},
        {"word": "scroll", "hint": "A rolled-up piece of parchment."},
        {"word": "climax", "hint": "The most exciting part of a story."},
        {"word": "parlor", "hint": "A living room or reception area."},
        {"word": "gallop", "hint": "A horse’s fastest run."},
        {"word": "various", "hint": "Different kinds or types."}
    ],
    "expert": [
        {"word": "syllabic", "hint": "Relating to syllables in words."},
        {"word": "proximal", "hint": "Closer to the center of the body."},
        {"word": "morphic", "hint": "Relating to form or shape."},
        {"word": "coprolalia", "hint": "Involuntary swearing, often linked to Tourette’s."},
        {"word": "vocabulary", "hint": "All the words a person knows."},
        {"word": "microcosm", "hint": "A small version of a larger world."},
        {"word": "scrollwork", "hint": "Decorative carving with curves."},
        {"word": "villarosa", "hint": "A place name; ‘villa’ means house."},
        {"word": "colossal", "hint": "Extremely large in size."},
        {"word": "polymorphic", "hint": "Having many different forms."}
    ]
}

# Scoreboard
score = {"wins": 0, "losses": 0}


#Game class
class HangmanGame:
    def __init__(self, root):
        self.root = root
        self.root.title("Lifeline letters")

        #Initialize game variables
        self.mode = None
        self.difficulty = None
        self.word_data = None
        self.word = ""
        self.hint = ""
        self.display_word = []
        self.guessed_letters = []
        self.lives = 6

        #Start screen
        self.create_start_screen()

    def clear_screen(self): #Clear the widgets in the screen
        for widget in self.root.winfo_children():
            widget.destroy()

    def create_start_screen(self): #Start screen
        self.clear_screen()
        background_label = Label(root, image=photo)
        background_label.image = photo  # To keep a reference

        background_label.place(x=0, y=0, relwidth=1, relheight=1) #Apply background image
        self.score_label = Label(root, text=f"Wins: {score['wins']} | Losses: {score['losses']}", font=subheadingFont,fg="#DC143C").place(x = 550,y=600) #Display scoreboard
        Button(self.root, text="Play Game", command=self.choose_difficulty, font = buttonFont,fg="white", bg="red").place(x = 600,y=640)

    def choose_difficulty(self): #Choose difficulty mode
        self.clear_screen()
        Label(self.root, text="Choose Difficulty:", font=headingFont).pack(pady=100)
        for difficulty in ["easy", "medium", "hard", "expert", "random"]: #Buttons for each difficulty mode
            Button(self.root, text=difficulty.capitalize(),command=lambda d=difficulty: self.finalize_mode(d), font = buttonFont,fg="white", bg="red").pack(pady=10)

    def finalize_mode(self, difficulty): #To finalize difficulty mode
        self.difficulty = difficulty
        if difficulty == "random": #If random mode is chosen, the player is surprised with a random difficulty mode
            difficulty = random.choice(list(word_list.keys()))
        self.word_data = random.choice(word_list[difficulty]) #Choose a random word from the word list under the chosen difficulty mode
        self.start_game()

    def start_game(self): #Start the game
        self.word = self.word_data["word"]
        self.hint = self.word_data["hint"]
        self.display_word = ["_" for _ in self.word] #Dashes are displayed representing each letter of the secret random word
        self.guessed_letters = [] #List to store the guessed letters so that a reminder is displayed to the player if they repeat a letter
        self.lives = 6
        self.create_game_screen()

    def create_game_screen(self): 
        self.clear_screen()

        self.hearts_label = Label(self.root, text=heart_symbol * self.lives, font=("Arial", 50), fg="red") #Red hearts are shown as lives
        self.hearts_label.pack(pady=10)

        self.word_label = Label(self.root, text=" ".join(self.display_word), font=subheadingFont) #Dashes are displayed
        self.word_label.pack(pady=50)

        self.hint_label = Label(self.root, text=f"Hint: {self.hint}", font=bodyFont, fg = "#DC143C", bg="white").pack(pady=50) # Hint 

        Button(self.root, text="Guess", command=self.make_guess, font = buttonFont, fg="white", bg="red").pack(pady=100) #Pressing the guess button will prompt the user to input their guess in the M5Stack Core2

    def make_guess(self):

        time.sleep(2)
        incoming = str((ArduinoSerial.readline()).decode('utf-8')) #Read the incoming serial input from the M5Stack Core2
        guess = incoming.rstrip() # Remove whitespaces as well as newline characters

        if guess in self.guessed_letters: #A reminder is shown if the guessed letter is repeated
            messagebox.showinfo("Repeated", "You already guessed that letter.")
            return

        self.guessed_letters.append(guess)

        if guess in self.word: #Reveal correct letters
            for i, letter in enumerate(self.word):
                if letter == guess:
                    self.display_word[i] = guess
        else:
            self.lives -= 1 #A life is lost for each incorrect guess

        self.update_screen()

    def update_screen(self):
        self.word_label.config(text=" ".join(self.display_word))
        self.hearts_label.config(text=heart_symbol * self.lives)

        if "_" not in self.display_word: #Check win
            score["wins"] += 1
            messagebox.showinfo("Game Over", f"You won! The word was '{self.word}'.")
            self.create_start_screen()
        elif self.lives <= 0: #Check loss
            score["losses"] += 1
            messagebox.showinfo("Game Over", f"You lost! The word was '{self.word}'.")
            self.create_start_screen()
    

        
#Establish a serial connection with Arduino
ArduinoSerial=serial.Serial("your serial communication port","Baud rate")

#Create a Tkinter window
root = Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.geometry("%dx%d" %(screen_width,screen_height))
root.minsize(600,600) #The Tkinter window can be minimized up to 600x600

# Fonts
buttonFont = font.Font(family='Arial', size=18, weight='bold')

headingFont = font.Font(family='Arial', size=22, weight='bold')
subheadingFont = font.Font(family='Arial', size=20, weight='bold')
bodyFont = font.Font(family='Arial', size=16, weight='bold')

heart_symbol = "\u2665 " # To denote the number of lives using hearts

#Background image
image = Image.open("name of your background image file") 
photo = ImageTk.PhotoImage(image)

#Run the game
game = HangmanGame(root)
root.mainloop()

Lifeline Letters - Arduino

Arduino
This is the Arduino code for this project. Replace "Color_Initial_Letter_Recognition.ino" in the repository with this Arduino sketch.
#include <M5Core2.h>
#include <M5GFX.h>
#include "FastLED.h"
#include "neuton.h"
M5GFX display;

int val;
int iteration;
const int Buffer_Size = 450;
float* Buffer = (float*) calloc(Buffer_Size, sizeof(float));   // allocate memory for pixel buffer with 0s
int j;
void setup(void)
{
  Serial.begin(115200);
  M5.begin();
  display.init();
  display.setFont(&fonts::Font4);
  if(!Buffer){
    Serial.println("Failed to allocate memory");
  }

  if (!display.touch())
  {
    display.setTextDatum(textdatum_t::middle_center);
    display.drawString("Touch not found.", display.width() / 2, display.height() / 2);
  }

  display.setEpdMode(epd_mode_t::epd_fastest);
  display.startWrite();
  FastLED.addLeds<SK6812, DATA_PIN>(leds, NUM_LEDS);
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i].setRGB(20, 20, 20);
  }
  FastLED.show();
  delay(1000);
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CRGB::Black;
  }
  FastLED.show(); 
}

void loop(void)
{
  static bool drawed = false;
  lgfx::touch_point_t tp[3];

  int nums = display.getTouchRaw(tp, 3);
  if (nums)
  {
    display.convertRawXY(tp, nums);

    for (int i = 0; i < nums; ++i)
    {
      if ((tp[i].y*320 + tp[i].x) != val && iteration<Buffer_Size)
      {
        Buffer[iteration] = tp[i].y*320 + tp[i].x;
        val=Buffer[iteration];
        iteration++;
      }
    }
    display.display();

    display.setColor(display.isEPD() ? TFT_BLACK : TFT_WHITE);
    for (int i = 0; i < nums; ++i)
    {
      int s = tp[i].size + 3;
      switch (tp[i].id)
      {
      case 0:
        display.fillCircle(tp[i].x, tp[i].y, s);
        break;
      case 1:
        display.drawLine(tp[i].x-s, tp[i].y-s, tp[i].x+s, tp[i].y+s);
        display.drawLine(tp[i].x-s, tp[i].y+s, tp[i].x+s, tp[i].y-s);
        break;
      default:
        display.fillTriangle(tp[i].x-s, tp[i].y +s, tp[i].x+s, tp[i].y+s, tp[i].x, tp[i].y-s);
        break;
      }
      display.display();
    }
    drawed = true;
  }
  else if (drawed)
  {
    drawed = false;
    display.waitDisplay();
    display.clear();
    display.display();
    if (neuton_model_set_inputs(Buffer)==0){
      uint16_t index;
      float* outputs;
      if (neuton_model_run_inference(&index, &outputs) == 0){
        switch(index){
          case 0:
          Serial.println("a");
          break;
          case 1:
          Serial.println("b");
          break;
          case 2:
          Serial.println("c");
          break;
          case 3:
          Serial.println("f");
          break;
          case 4:
          Serial.println("g");
          break;
          case 5:
          Serial.println("i");
          break;
          case 6:
          Serial.println("l");
          break;
          case 7:
          Serial.println("m");
          break;
          case 8:
          Serial.println("o");
          break;
          case 9:
          Serial.println("p");
          break;
          case 10:
          Serial.println("r");
          break;
          case 11:
          Serial.println("s");
          break;
          case 12:
          Serial.println("v");
          break;
          case 13:
          Serial.println("w");
          break;
          case 14:
          Serial.println("y");
          break;
      }
    }
   }
   iteration = val = 0;
   neuton_model_reset_inputs();
   free(Buffer);
   Buffer = (float*) calloc(Buffer_Size, sizeof(float));
   if (!Buffer)
    {
      Serial.println("Failed to allocate memory");
    }
  }
  vTaskDelay(1);
}

Touch Test

Arduino
This is an example sketch file from M5GFX library that allows you to test your device and see how the touch points are drawn on the screen. Try using this example to understand how the device works and to verify that your device is working properly.
#include <M5GFX.h>

M5GFX display;

void setup(void)
{
  display.init();
  display.setFont(&fonts::Font4);

  if (!display.touch())
  {
    display.setTextDatum(textdatum_t::middle_center);
    display.drawString("Touch not found.", display.width() / 2, display.height() / 2);
  }

  display.setEpdMode(epd_mode_t::epd_fastest);
  display.startWrite();
}

void loop(void)
{
  static bool drawed = false;
  lgfx::touch_point_t tp[3];

  int nums = display.getTouchRaw(tp, 3);
  if (nums)
  {
    for (int i = 0; i < nums; ++i)
    {
      display.setCursor(16, 16 + i * 24);
      display.printf("Raw X:%03d  Y:%03d", tp[i].x, tp[i].y);
    }

    display.convertRawXY(tp, nums);

    for (int i = 0; i < nums; ++i)
    {
      display.setCursor(16, 128 + i * 24);
      display.printf("Convert X:%03d  Y:%03d", tp[i].x, tp[i].y);
    }
    display.display();

    display.setColor(display.isEPD() ? TFT_BLACK : TFT_WHITE);
    for (int i = 0; i < nums; ++i)
    {
      int s = tp[i].size + 3;
      switch (tp[i].id)
      {
      case 0:
        display.fillCircle(tp[i].x, tp[i].y, s);
        break;
      case 1:
        display.drawLine(tp[i].x-s, tp[i].y-s, tp[i].x+s, tp[i].y+s);
        display.drawLine(tp[i].x-s, tp[i].y+s, tp[i].x+s, tp[i].y-s);
        break;
      default:
        display.fillTriangle(tp[i].x-s, tp[i].y +s, tp[i].x+s, tp[i].y+s, tp[i].x, tp[i].y-s);
        break;
      }
      display.display();
    }
    drawed = true;
  }
  else if (drawed)
  {
    drawed = false;
    display.waitDisplay();
    display.clear();
    display.display();
  }
  vTaskDelay(1);
}

Data Collection

Arduino
Use this to collect data and prepare your dataset
#include <M5GFX.h>

M5GFX display;
int val;
int iteration;
const int Buffer_Size = "Your estimation of the number of pixels that are included when you draw a digit";
int* Buffer = (int*) calloc(Buffer_Size, sizeof(int));  

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  display.init();
  display.setFont(&fonts::Font4);

  if (!display.touch())
  {
    display.setTextDatum(textdatum_t::middle_center);
    display.drawString("Touch not found.", display.width() / 2, display.height() / 2);
  }

  display.setEpdMode(epd_mode_t::epd_fastest);
  display.startWrite();
  
  //Creating the header for the CSV file
  Serial.print("Label");
  for (int i=0;i<Buffer_Size;i++){
    Serial.print(",");
    Serial.print("pixel"+String(i));
  }
  Serial.println(); //Starts new row
  Serial.print("The digit for which you want to collect samples");
}

void loop() {
  // put your main code here, to run repeatedly:
  static bool drawed = false;
  lgfx::touch_point_t tp[3];

  int nums = display.getTouchRaw(tp, 3);

  if(nums)
  {
    display.convertRawXY(tp, nums);
    for (int i = 0; i < nums; ++i){
      if((tp[i].y * 320 + tp[i].x) != val && iteration < Buffer_Size){ //Prevents duplicate data
        Buffer[iteration] = (tp[i].y * 320) + tp[i].x; //Store pixel values. 320 is display width and can vary with different touchscreens
        val = Buffer[iteration];
        iteration++;
      }                 
     }
     display.display();
     display.setColor(display.isEPD() ? TFT_BLACK : TFT_WHITE);
     for (int i = 0; i < nums; ++i)
     {
      int s = tp[i].size + 3;
      switch (tp[i].id)
      {
      case 0:
        display.fillCircle(tp[i].x, tp[i].y, s);
        break;
      case 1:
        display.drawLine(tp[i].x-s, tp[i].y-s, tp[i].x+s, tp[i].y+s);
        display.drawLine(tp[i].x-s, tp[i].y+s, tp[i].x+s, tp[i].y-s);
        break;
      default:
        display.fillTriangle(tp[i].x-s, tp[i].y +s, tp[i].x+s, tp[i].y+s, tp[i].x, tp[i].y-s);
        break;
      }
      display.display();
    }
    drawed = true;
   }

   else if (drawed) //Implements after you finish drawing the digit
   {   
    for(int i = 0; i < Buffer_Size; i++){
      Serial.print(",");
      Serial.print(Buffer[i]);
      
    }
    Serial.println(); //Create a new row
    Serial.print("The digit for which you want to collect samples");

    drawed = false;
    display.waitDisplay();
    display.clear(); //Clear display to draw digit again
    display.display();
    val=iteration=0; //Reset iteration and val variables
    free(Buffer); //Clear buffer to allocate memory for the digits drawn again
    Buffer= (int*) calloc(Buffer_Size, sizeof(int));     
   }
   vTaskDelay(1);    
}

Handwritten Letters recognition

This was the TinyML model that I used in a previous project of mine. Recognize handwritten letters with Neuton TinyML. This repository contains the C library (TinyML model) and the Arduino sketch file that can be used to embed the model in your microcontroller.

Credits

Rucksikaa Raajkumar
44 projects • 96 followers
Amateur Arduino Developer. Undergraduate. YouTuber (https://www.youtube.com/c/RucksikaaRaajkumar/videos) and Blogger (Arduino Projects by R)

Comments