No tutorial de hoje, abordaremos detalhadamente o processo de construção do Jogo Genius em uma versão 2.0, que conta com o ESP32-C3 Super mini. Ele permite trazer ao nosso projeto, alta capacidade de processamento em tamanho reduzido.

O Genius 2.0 com ESP32-C3 é uma versão atualizada, do icônico jogo de memória. Ele combina a eletrônica e programação para criar uma experiência interativa e educativa, além de muita diversão! O objetivo do jogo é simples: o jogador deve repetir uma sequência de luzes e sons, que aumenta em complexidade conforme o progresso no jogo.

Neste tutorial, abordaremos detalhadamente o processo de construção do Genius 2.0, definiremos os componentes que serão utilizados, criaremos o esquema elétrico, montagem do circuito e programação do código. Além disso, veremos como o uso de tecnologias modernas, como o display OLED, tornam o projeto mais interativo e dinâmico ao jogador. 

O tutorial de hoje será divido nos seguintes tópicos. Caso deseje ir para algum específico, basta clicar em seu respectivo tópico: 

Lista de Materiais

Como o Jogo Genius Funciona

Quais componentes os necessários para construir um Genius 2.0?

Diagrama Elétrico

Montagem do Circuito

Como programar um jogo Genius do Zero?

Código Completo Genius 2.0 com ESP32

Video demonstração do projeto do Genius 2.0

Conclusão
 

Para construir o Genius 2.0, utilizemos os seguintes materiais:  

Lista de Materiais

01 - Placa Super Mini ESP32-C3

01 - Display OLED 128x64 Px - 1.3" - 4 Pin - Branco

04 - LEDs - 1 verde / 1 vermelho / 1 azul/ 1 amarelo

04 - Chave Táctil - 12x12x7,3 mm

04 - Capa A24 Para chave táctil - 1 verde / 1 vermelha / 1 azul/ 1 amarela

01 - Kit Jumpers Rígidos Formato de U - 140 pçs

04 - Resistores de 1KΩ para LEDs

01 - Buzzer 12mm - Sem Oscilador Interno - 5V

01 -Protoboard de 830 pontos

 
Como o Jogo Genius Funciona

O jogo Genius é um famoso jogo, que ficou popular na década de 1980. Sua jogabilidade é simples, o jogador deve memorizar a sequência de luzes e sons que se torna mais extensa e rápida a cada rodada. Sua versão original possuia três jogos diferentes e quatro níveis de dificuldade. 

Fonte: Estrela Brinquedos

 

A versão 2.0 do Genius, é uma versão aprimorada do clássico citado acima. Seguindo a mesma linha de raciocínio, o jogador tem que seguir uma sequência de cores que aumenta conforme o nível avança. Primeiro a sequência aumenta e quando certo nível é atingido, a velocidade também é aumentada, aumentando a dificuldade conforme se passam os rounds. 

Em nossa versão, o ESP32-C3 vai controlar os LEDs que representam as cores do jogo, e receber a leitura dos botões, que serão registrados para comparar com a sequência demonstrada, além de também contar com feedback auditivo para aumentar a interatividade com o jogador. Além disso, com o ESP32-C3 Super Mini também possível armazenar os valores do Record atual dos jogadores, demonstrando o teto dos pontos que o jogar com maior recorde fez, além de também, ser possível apagar o recorde anterior. 

 

Quais componentes os necessários para construir um Genius 2.0?

A escolha dos componentes para o Genius 2.0 é crucial para garantir um projeto funcional e eficiente. Para começarmos, iremos definir primeiro o microcontrolador: ESP32-C3 Super Mini. Você pode se perguntar, por que não utilizar um Arduino Uno ou um Arduino Pro mini?

A resposta é bem simples. A placa Super Mini ESP32-C3 consegue unificar três características essenciais para o nosso projeto: Tamanho reduzido, Capacidade de processamento e quantidade de memória. Com isso, o ESP32-C3 é a escolha ideal para ser o cérebro do projeto. Ele fica responsável pelo controle dos LEDs, recebimento dos comandos dos botões, escrita na tela e também ativação do buzzer para retorno sonoro.

Placa Super Mini ESP32-C3

Já na escolha do LCD, você pode se perguntar, porque não utilizamos o Display LCD 16x2 com I2C. De mesmo modo que o ESP32-C3 Super Mini, o Display OLED 128x64 Px - 1.3" - 4 Pin - Branco consegue trazer duas incríveis características para o nosso projeto: tamanho reduzido e interatividade aumentada. Com ele é possível demonstrar diversas informações na tela ao mesmo tempo, como o Recorde, mensagens de acerto e timing para o jogador realizar a ação. Além dele também utilizar poucos fios de comunicação com o microcontrolador.

Display OLED 128x64 Px - 1.3" - 4 Pin - Branco

Para permitir as conexões do projeto, e também facilitar a prototipagem, uma protoboard de 830 pontos deve ser utilizada para montagem do circuito. Por ser maior, essa protoboard consegue unificar todo o projeto em um só, permitindo a portabilidade e segurança do Genius 2.0.  

Protoboard 830 Pontos

Com esses componentes, já é possível testar a interface de telas que aparecem no display. Agora partiremos para os componentes que serão utilizados na montagem do circuito que será responsável por representar o Genius.

Para as cores, utilizaremos um LED de cada cor, sendo eles, um LED Verde, um LED Vermelho, um led Azul e um led Amarelo. Para limitar seu uso de corrente, um resistor de 1K é necessário em cada um dos LEDs.

Já para os botões, utilizaremos quatro chaves tácteis e também quatro capas para cada uma das chaves:

 

Além disso, também utilizaremos o Kit Jumpers Rígidos Formato de U - 140 pçs para interligarmos todo o projeto.

Por fim, vamos utilizar um Buzzer 12MM Sem Oscilador Interno - 5V que é capaz de reproduzir diferentes frequências de som, podendo simular a escala musical que será utilizada.

Com todos os componentes em mãos, partiremos para a criação do esquema elétrico do Genius 2.0. 

Diagrama Elétrico

Nessa etapa do projeto, uniremos todos os periféricos ao microcontrolador. Aqui dividiremos em duas etapas. A primeira delas é a definição do Pinout, e a segunda etapa é a criação dos diagramas elétricos, que serão dois. O primeiro diagrama será construído para melhor entendimento do circuito elétrico. Já o segundo esquema elétrico, será como foi construído fisicamente para melhorar a jogabilidade e aparência do projeto. 

Veja abaixo o pinout:

ComponentesPlaca ESP32-C3 Super Mini
Botão Amarelo0
Botão Verde1
Botão Vermelho2
Botão Azul3
Buzzer4
Led Amarelo5
Led Azul6
Led Vermelho7
Display OLED SDA8
Display OLED SCL9
Led Verde10

 

Veja o primeiro esquema elétrico, onde todas as conexões estão interligadas diretamente para proporcionar melhor visualização:

Já o segundo esquema elétrico, é a forma como ele foi montado na protoboard, e o esquema elétrico ficou da seguinte maneira:

Caso você deseje seguir a mesma montagem que a nossa, pode seguir a seguinte ligação dos fios:

Montagem do Circuito

Seguindo a mesma sequência de fios do diagrama elétrico da montagem real, a nossa montagem ficou da seguinte maneira:

Como programar um jogo Genius do Zero?

Primeiro de tudo, durante a criação do Genius 2.0, alguns desafios foram propostos. O primeiro desafio era que o jogo deveria executar as mesmas funções do jogo original, com sequência de cores aleatórias, botões para resposta das cores e som para feedback sonoro. O segundo desafio era adicionar um OLED para demonstração dos níveis e recorde atual no jogo. Já no terceiro desafio, era necessário um recorde que não apagasse quando o microcontrolador é reiniciado. 
Com todos esses desafios em mente, começamos o código da seguinte maneira:

Primeiro, definimos todas as bibliotecas que serão utilizadas no código. A primeira delas é a <EEPROM.h>. Essa biblioteca é responsável por permitir a gravação na memória EEPROM do ESP32-C3. Mas afinal, o que isso significa? 

Gravar algum dado na memória EEPROM, faz com que, esse dado permaneça guardado mesmo quando não há energia no microcontrolador. A memória EEPROM grava em um espaço físico do microcontrolador, na posição desejada. Mas fique atento, a quantidade de gravações que são possíveis realizar dentro desse espaço são limitadas e uma hora, gravar na mesma posição se tornará impossível. 

A próxima biblioteca a ser incluída é a <Wire.h>. Ela é responsável por controlar a comunicação I2C do microcontrolador com o display. Ela habilita a linha do I2C do microcontrolador e permite utilizarmos apenas dois fios para comunicação: SDA e SCL. 

Por fim, a última biblioteca adicionada é a do display: "SH1106Wire.h". Ela é responsável pelo display OLED. Auxilia na comunicação do microcontrolador com o display com diversas funções que facilitam a utilização, desenho, escrita, etc, de tudo feito na tela do display.

#include <EEPROM.h>
#include <Wire.h>
#include <SH1106Wire.h>

Após isso utilizaremos o #define para o controle dos LEDs, dos botões e do buzzer, além de também utilizarmos para o tempo do Led Aceso, endereço de inicialização, endereço Recorde e valor Inicializado. 

O #define é um macro, que substitui todas as ocorrências pelo valor definido. 

Exemplo:

#define ledAmarelo 5

Esse macro define que, todas as ocorrências de ledAmarelo no código, serão substituídas pelo valor de 5 antes da compilação.  

Após isso, variáveis do tipo inteiro são definidas:

// Variáveis para o jogo
int recorde = 0;              // Variável para armazenar o recorde
int sequencia[100];           // Array para armazenar a sequência do jogo
int tamanhoSequencia = 0;     // Controla o tamanho da sequência
int pontuacao = 0;            // Pontuação do jogador

Essas variáveis definem:

O recorde como 0 na primeira vez que o jogo é iniciado. 

A Sequência como um array de 100 espaços para armazenar o valor da sequência do jogo.

O tamanhoSequencia controla o tamanho dessa sequência do jogo

E pontuação como 0, para sempre definir a pontuação inicial do jogador como 0.

A próxima linha, cria um objeto chamado display da classe SH1106Wire. Esse objeto será utilizado para chamar as funções disponíveis da classe, como por exemplo display.init(); Após isso, são definidos os parametrôs de comunicação com o display, que são parametros definidos para o construtor da classe SH1106Wire. Veja abaixo como fica a linha de código:

SH1106Wire display(0x3C, SDA, SCL);

Em seguida, entramos na função Setup, e iniciamos a EEPROM do ESP com a seguinte linha de comando EEPROM.begin(512). O 512 é utiilizado para definir o tamanho em bytes que será utilizado para memória. Após isso, o código verifica se a EEPROM foi inicializada e caso não tenha sido, ele inicia a EEPROM definindo e gravando o valor inicial como 0 em EEPROM.comit();

Se a EEPROM já tiver iniciado, o código carrega o valor do recorde. 

void setup() 
{ // Inicializa a EEPROM com tamanho 512 bytes EEPROM.begin(512); // Verifica se a EEPROM já foi inicializada if (EEPROM.read(enderecoInicializacao) != VALOR_INICIALIZADO) { // Se não foi inicializada, define o valor inicial e grava o recorde inicial como 0 EEPROM.write(enderecoInicializacao, VALOR_INICIALIZADO); EEPROM.write(enderecoRecorde, 0); // Recorde inicial = 0 EEPROM.commit(); // Grava as alterações recorde = 0; } else { // Se a EEPROM já foi inicializada, carrega o valor do recorde recorde = EEPROM.read(enderecoRecorde); }

Seguimos com a definição dos pinos dos Leds e dos botões. Os botões são definidos como INPUT_PULLUP pois utilizam os resistores internos do microcontrolador e eles enviarão o sinal de aperto do botão. Logo após isso iniciamos o display com display.init(); e invertemos a tela com display.flipScreenVertically(); e seguindo o código, apenas escrevemos Bem-vindo ao Genius Game! de uma maneira que fique ajustada a tela do dispaly OLED, além de também escrever o recorde atual. Por fim o display será limpado com display.clear(); e chamamos a função esperarNovoJogo(), que vai aguardar o pressionamento de um botão para iniciar o jogo. Toda essa lógica fica da seguinte maneira:

  // Configuração dos pinos dos LEDs como saída
  pinMode(ledAmarelo, OUTPUT);
  pinMode(ledAzul, OUTPUT);
  pinMode(ledVerde, OUTPUT);
  pinMode(ledVermelho, OUTPUT);

  // Configuração dos pinos dos botões como entrada pull-up
  pinMode(botaoAmarelo, INPUT_PULLUP);
  pinMode(botaoAzul, INPUT_PULLUP);
  pinMode(botaoVerde, INPUT_PULLUP);
  pinMode(botaoVermelho, INPUT_PULLUP);

  // Inicializa o display OLED
  display.init();
  display.flipScreenVertically();
  display.setFont(ArialMT_Plain_16);

  // Exibe mensagem de boas-vindas no OLED
  display.drawString(12, 15, "Bem-vindo ao");
  display.drawString(10, 35, "Genius Game!");
  display.display();
  delay(2000);
  display.clear();

  // Exibe o recorde atual
  display.drawString(12, 15, "Recorde Atual:");
  display.drawString(54, 35, String(recorde));
  display.display();
  delay(2000);
  display.clear();

  // Espera pelo jogador para iniciar a partida
  esperarNovoJogo();
}

Após terminarmos o Setup, iniciamos o Void Loop, chamando duas funções mostrarNivelAtual(); e mostrarSequencia(); que são responsáveis por escrever o nível e a sequencia de leds que acenderá.

Seguindo dentro do Void Loop, iniciamos uma lógica para verificação da entrada do jogador. Primeiro, se o jogador acertar, chama a função atualizarPontuacao(); que atualiza a quantidade de pontos que o jogador ganha por rodada conforme a progressão aumenta, seguido de uma limpeza na tela e escrita de "acertou mizeravi" e "prepare-se" para descontrair com o jogador. Após essa mensagem, a função que mostra a pontuação máxima em todas as telas é chamada com displayPontuacao(); Com isso display.display é chamada para limpar a tela e display.clear é chamada para atualizar o buffer. Por ultimo, chamamos a função responsável por gerar a sequência dos Leds com a função gerarSequencia();

Se o jogador não acertar em verificarEntradaJogador(), a tela é limpa, é escrito uma mensagem de Game Over, a tela é atualizada e a música de derrota é tocada através da função tocarMusicaDerrota(); 

Por fim, a pontuação do jogador é comparada com o recorde atual, e se ela for maior que o recorde atual, o recorde adquire o valor da pontuação e é gravado na memória EEPROM e uma mensagem de novo recorde é escrita na tela. Se a pontuação não ultrapassar o recorde atual, somente o valor do Recorde Atual é mostrada na pontuação. 

Finalizando o Loop, a pontuação do jogador é zerado. A função esperarNovoJogo() é chamada para aguardar a função do jogador. 

void loop() {
  
  mostrarNivelAtual();// Exibe o nível atual antes de mostrar a sequência
  mostrarSequencia();// Mostra a sequência ao jogador
  
  // Verifica a entrada do jogador
  if (verificarEntradaJogador()) {
    atualizarPontuacao();  // Atualiza a pontuação conforme a progressão
    display.clear();
    display.drawString(5, 20, "Acerto Mizeravi");
    display.drawString(20, 40, "Prepare-se");
    displayPontuacao();  // Mostra a pontuação em todas as telas do jogo
    display.display();
    delay(1000);
    display.clear();
    gerarSequencia();
  } else {
    display.clear();
    display.drawString(20, 25, "Game Over!");
    display.display();
    delay(1000);
    display.clear();
    tocarMusicaDerrota();

    // Verifica se o jogador atingiu um novo recorde
    if (pontuacao > recorde) 
    {
      recorde = pontuacao; 
      EEPROM.write(enderecoRecorde, recorde); 
      EEPROM.commit(); // Grava o novo recorde na EEPROM
      display.clear();
      display.drawString(15, 15, "Novo Recorde:");
      display.drawString(55, 35, String(recorde));
      display.display();
      delay(1000);
      display.clear();
    } 
    else 
    {
      display.clear();
      display.drawString(10, 15, "Recorde Atual:");
      display.drawString(55, 35, String(recorde));
      display.display();
      delay(1000);
      display.clear();
    }

    // Zera a pontuação do jogador ao perder
    pontuacao = 0;
    esperarNovoJogo();
  }
}

Passando rápidamente pelas funções temos a função atualizaPontuacao() que verifica de maneira simples o nível que o jogador está, e a partir desse nível define a pontuação que ele recebe. Se o nível atual for menor que 10, o jogador recebe 1 ponto, se o nível estiver entre 11 e 19 o jogador receberá 2 pontos, e se o nível atual for maior ou igual a 20 o jogador vai ganhar 5 pontos por acerto. 

void atualizarPontuacao() {
  if (tamanhoSequencia >= 20) {
    pontuacao += 5;  // A partir do nível 20, 5 pontos por acerto
  } else if (tamanhoSequencia >= 11) {
    pontuacao += 2;  // A partir do nível 10, 2 pontos por acerto
  } else {
    pontuacao += 1;  // Até o nível 9, 1 ponto por acerto
  }
}

Já a função int calcularTempoLedAceso() é utilizada para calcular a velocidade que o LED fica aceso conforme o nível que o jogador está.

int calcularTempoLedAceso() {
  if (tamanhoSequencia >= 20) {
    return tempoLedAceso / 2; // A partir do nível 20, LEDs 3 vezes mais rápidos
  } else if (tamanhoSequencia >= 10) {
    return tempoLedAceso / 1.5; // A partir do nível 10, LEDs 2 vezes mais rápidos
  } else {
    return tempoLedAceso; // Até o nível 9, o tempo é o padrão
  }
}

Em sequência temos a função mostrarSequencia() que acende os leds e executa a função acenderLed com a sequência gerada anteriormente dentro do for e apagarLeds  para desligar os Leds. Entre essas funções o tempo de Led aceso é utilizado como delay.

void mostrarSequencia() {
  int tempoAtualLedAceso = calcularTempoLedAceso(); // Calcula o tempo baseado no nível atual
  for (int i = 0; i < tamanhoSequencia; i++) {
    acenderLed(sequencia[i]); 
    delay(tempoAtualLedAceso); // Aguarda pelo tempo ajustado entre o acionamento de cada LED
    apagarLeds();         
    delay(tempoAtualLedAceso); // Pausa ajustada entre cada LED na sequência
  }
}

Na função acenderLed() acionaremos o LED e o buzzer ao mesmo tempo através do switch case, e na função apagarLeds()  os leds são simplesmente apagados e o buzzer é definido sem som. 

void acenderLed(int led) {
  switch (led) {
    case 0:
      digitalWrite(ledAmarelo, HIGH);
      tone(buzzer, notaAmarelo);
      break;
    case 1:
      digitalWrite(ledAzul, HIGH);
      tone(buzzer, notaAzul);
      break;
    case 2:
      digitalWrite(ledVerde, HIGH);
      tone(buzzer, notaVerde);
      break;
    case 3:
      digitalWrite(ledVermelho, HIGH);
      tone(buzzer, notaVermelho);
      break;
  }
}

void apagarLeds() {
  digitalWrite(ledAmarelo, LOW);
  digitalWrite(ledAzul, LOW);
  digitalWrite(ledVerde, LOW);
  digitalWrite(ledVermelho, LOW);
  noTone(buzzer);
}

Na função gerarSequencia() um número aleatório é gerado e adicionado a um vetor chamado sequência. Ele gera a função random(4), que gera um número aleatório entre 0 e 3, e atribui esse valor ao próximo índice disponível no vetor sequência.  Esse valor é armazenado na posição do vetor indicada por tamanhoSequencia, que representa o tamanho atual da sequência. Após adicionar o número à sequência, a variável tamanhoSequencia é incrementada, permitindo que, na próxima vez que a função for chamada, o novo número seja adicionado ao final da sequência.

void gerarSequencia(){
  sequencia[tamanhoSequencia] = random(4); // Gera um número aleatório entre 0 e 3
  tamanhoSequencia++;
}

A função tocarMusicaAbertura()  chama a função acenderLed com um valor de argumento, e esse valor será o que vai definir dentro do switch case da função acenderLed, qual led será aceso. 

void tocarMusicaAbertura() {
  acenderLed(2);
  delay(300);
  apagarLeds();
  delay(100);

  acenderLed(3);
  delay(300);
  apagarLeds();
  delay(100);

  acenderLed(1);
  delay(300);
  apagarLeds();
  delay(100);

  acenderLed(0);
  delay(300);
  apagarLeds();
  delay(1000);  // Espera um segundo antes de iniciar a partida
}

TocarMusicaDerrota() é uma função que define primeiro, as notas que serão tocadas quando o jogador perder. Em seguida a duração de cada nota  é definida. Após isso, há um laço For que realiza a comparação do valor da variavel criada i. Ele é criado dessa maneira, para prender o código dentro desse laço enquanto i for menor que 8. Enquanto essa condição for verdadeira, o código irá tocar as notas, e acender todos os LEDS ao mesmo tempo para sinalizar que o jogo foi encerrado. 

void tocarMusicaDerrota() 
{
int notas[] = {261, 277, 311, 349, 392, 415, 466, 261}; // Notas da escala C Frígio, terminando na tônica
int duracoes[] = {200, 150, 200, 150, 300, 200, 200, 400}; // Duração de cada nota


  for (int i = 0; i < 8; i++) {
    tone(buzzer, notas[i]);
    digitalWrite(ledVermelho, HIGH);
    digitalWrite(ledAmarelo, HIGH);
    digitalWrite(ledAzul, HIGH);
    digitalWrite(ledVerde, HIGH);
    delay(duracoes[i]);
    noTone(buzzer);    
    apagarLeds();
    delay(100); // Pequena pausa entre as notas
  }

  delay(1000); // Espera um segundo antes de permitir iniciar uma nova partida
}

As funções displayPontuacao() mostrarNivelAtual() são somente funções de escrita no display. 

void displayPontuacao() {
  display.setFont(ArialMT_Plain_10);
  display.drawString(75, 0, "Pontos: " + String(pontuacao));
  display.drawString(0, 0, "Record: " + String(recorde));
  display.setFont(ArialMT_Plain_16);
}

void mostrarNivelAtual() {
  display.clear();
  display.drawString(32, 25, "Nível: " + String(tamanhoSequencia));
  displayPontuacao();
  display.display();
  delay(500);
}

Ao final do código, temos a função verificarEntradaJogador(). Essa função escreve sua vez no display e para cada elemento da sequência (sequencia[i]), a função chama esperarBotaoPressionado(), que aguarda o jogador pressionar um botão. Se a sequência de botões pressionados não corresponder à sequência gerada, a função retorna false. Caso contrário, retorna true.

bool verificarEntradaJogador() {
  display.clear();
  display.drawString(32, 24, "Sua vez");
  displayPontuacao();
  display.display();
  delay(100);  // Pausa para o jogador se preparar
  display.clear();

  for (int i = 0; i < tamanhoSequencia; i++) {
    int botaoPressionado = esperarBotaoPressionado();
    if (botaoPressionado != sequencia[i]) {
      return false; // Entrada incorreta
    }
  }
  return true; // Entrada correta
}

A função esperarBotaoPressionado();  fica em loop, aguarda um botão ser pressionado. Quando o botão é pressionado, a função acende o LED correspondente, espera por um tempo em tempoLedAceso e depois apaga os LEDs. A função retorna um valor inteiro (0,1,2 ou 3) correspondente ao botão que foi pressionado. 

int esperarBotaoPressionado() {
  while (true) {
    if (digitalRead(botaoAmarelo) == LOW) {
      acenderLed(0);
      delay(tempoLedAceso);
      apagarLeds();
      while (digitalRead(botaoAmarelo) == LOW) {};
      return 0;
    }
    if (digitalRead(botaoAzul) == LOW) {
      acenderLed(1);
      delay(tempoLedAceso);
      apagarLeds();
      while (digitalRead(botaoAzul) == LOW) {};
      return 1;
    }
    if (digitalRead(botaoVerde) == LOW) {
      acenderLed(2);
      delay(tempoLedAceso);
      apagarLeds();
      while (digitalRead(botaoVerde) == LOW) {};
      return 2;
    }
    if (digitalRead(botaoVermelho) == LOW) {
      acenderLed(3);
      delay(tempoLedAceso);
      apagarLeds();
      while (digitalRead(botaoVermelho) == LOW) {};
      return 3;
    }
  }
}

Por fim, no final do código, temos a última função esperarBotaoPressionado(). Nela é escrita o recorde, Genius Game e Iniciar Jogo. Em seguida, entramos em um loop de espera infinita que se todos os botões forem pressionados ao mesmo tempo o recorde gravado na memória é zerado e a mensagem confirmando o Reset do recorde é mostrada. 

Após o reset, a função esperarNovoJogo() é chamada novamente, reinicinado o ciclo de espera para um novo jogo ou outro reset.

Caso somente um botão seja apertado, o jogo é iniciado e é aguardado o botão ser soltado. Ao final o código definite o tamanhoSequencia como zero, e chama a função gerarSequencia(), tocarMusicaAbertura(); Com um break ao final para para o While.

void esperarNovoJogo() {
  display.setFont(ArialMT_Plain_10); // Fonte menor para a pontuação
  display.drawString(0, 0, "Record: " + String(recorde));
  delay(500);
  display.setFont(ArialMT_Plain_16);
  display.drawString(12, 15, "Genius Game");
  display.drawString(20, 35, "Iniciar Jogo");
  display.display();
  display.clear();

  // Criamos uma espera "infinita" por um botão ou todos juntos para resetar
  while (true) {
    if (digitalRead(botaoAmarelo) == LOW && digitalRead(botaoAzul) == LOW &&
        digitalRead(botaoVerde) == LOW && digitalRead(botaoVermelho) == LOW) {
      // Todos os botões pressionados, resetar a EEPROM
      EEPROM.write(enderecoRecorde, 0); // Recorde resetado
      EEPROM.commit();                  // Grava na EEPROM
      recorde = 0;                      // Atualiza o recorde em RAM
      pontuacao = 0;
      tamanhoSequencia = 0;
      display.clear();
      display.drawString(28, 15, "Recorde");
      display.drawString(25, 35, "Resetado");
      display.display();
      delay(2000);
      display.clear();
      esperarNovoJogo(); 
      return; // Sai da função para recomeçar
    }

    // Se um botão for pressionado (iniciar o jogo)
    if (digitalRead(botaoAmarelo) == LOW || digitalRead(botaoAzul) == LOW ||
        digitalRead(botaoVerde) == LOW || digitalRead(botaoVermelho) == LOW) {
      // Aguarda o botão pressionado ser solto.
      while (digitalRead(botaoAmarelo) == LOW || digitalRead(botaoAzul) == LOW ||
             digitalRead(botaoVerde) == LOW || digitalRead(botaoVermelho) == LOW) {};
      tamanhoSequencia = 0; // Reinicia o tamanho da sequência
      gerarSequencia(); // Gera uma nova sequência
      tocarMusicaAbertura(); // Toca a música de abertura com animação de LEDs 
      break;         
    }
  }
}

Com isso, encerramos a explicação do código do jogo Genius 2.0. Veja como fica o código completo do jogo:

Código Completo Genius 2.0 com ESP32

#include <EEPROM.h>
#include <Wire.h>
#include <SH1106Wire.h> // Biblioteca ThingPulse para o display SH1106


// Definição dos pinos dos LEDs
#define ledAmarelo 5
#define ledAzul 6
#define ledVerde 10
#define ledVermelho 7

// Definição dos pinos dos botões
#define botaoAmarelo 0
#define botaoAzul 3
#define botaoVerde 1
#define botaoVermelho 2

// Definição do pino do buzzer
#define buzzer 4

// Notas musicais correspondentes aos LEDs
#define notaAmarelo  349 // Dó Fá
#define notaAzul  330   // Ré Mi
#define notaVerde 262   // Mi Dó
#define notaVermelho 294// Fá Ré

// Tempo que o LED fica aceso
#define tempoLedAceso 300 // em milissegundos

// Endereços na EEPROM
#define enderecoInicializacao 0 // Endereço para verificar a inicialização
#define enderecoRecorde 1       // Endereço para armazenar o recorde

// Valor de inicialização
#define VALOR_INICIALIZADO 123

// Variáveis para o jogo
int recorde = 0;              // Variável para armazenar o recorde
int sequencia[100];           // Vetor para armazenar a sequência do jogo
int tamanhoSequencia = 0;     // Controla o tamanho da sequência
int pontuacao = 0;            // Pontuação do jogador


// Inicializa o display OLED I2C SH1106
SH1106Wire display(0x3C, SDA, SCL); // Endereço I2C pode variar, mas 0x3C é comum para SH1106

void setup() {
  // Inicializa a EEPROM com tamanho 512 bytes
  EEPROM.begin(512);

  // Verifica se a EEPROM já foi inicializada
  if (EEPROM.read(enderecoInicializacao) != VALOR_INICIALIZADO) {
    // Se não foi inicializada, define o valor inicial e grava o recorde inicial como 0
    EEPROM.write(enderecoInicializacao, VALOR_INICIALIZADO);
    EEPROM.write(enderecoRecorde, 0); // Recorde inicial = 0
    EEPROM.commit();                   // Grava as alterações
    recorde = 0;
  } else {
    // Se a EEPROM já foi inicializada, carrega o valor do recorde
    recorde = EEPROM.read(enderecoRecorde);
  }

  // Configuração dos pinos dos LEDs como saída
  pinMode(ledAmarelo, OUTPUT);
  pinMode(ledAzul, OUTPUT);
  pinMode(ledVerde, OUTPUT);
  pinMode(ledVermelho, OUTPUT);

  // Configuração dos pinos dos botões como entrada pull-up
  pinMode(botaoAmarelo, INPUT_PULLUP);
  pinMode(botaoAzul, INPUT_PULLUP);
  pinMode(botaoVerde, INPUT_PULLUP);
  pinMode(botaoVermelho, INPUT_PULLUP);

  // Inicializa o display OLED
  display.init();
  display.flipScreenVertically();
  display.setFont(ArialMT_Plain_16);

  // Exibe mensagem de boas-vindas no OLED
  display.drawString(12, 15, "Bem-vindo ao");
  display.drawString(10, 35, "Genius Game!");
  display.display();
  delay(2000);
  display.clear();

  // Exibe o recorde atual
  display.drawString(12, 15, "Recorde Atual:");
  display.drawString(54, 35, String(recorde));
  display.display();
  delay(2000);
  display.clear();

  // Espera pelo jogador para iniciar a partida
  esperarNovoJogo();
}

void loop() {
  
  mostrarNivelAtual();// Exibe o nível atual antes de mostrar a sequência
  mostrarSequencia();// Mostra a sequência ao jogador
  
  // Verifica a entrada do jogador
  if (verificarEntradaJogador()) {
    atualizarPontuacao();  // Atualiza a pontuação conforme a progressão
    display.clear();
    display.drawString(5, 20, "Acerto Mizeravi");
    display.drawString(20, 40, "Prepare-se");
    displayPontuacao();  // Mostra a pontuação em todas as telas do jogo
    display.display();
    delay(1000);
    display.clear();
    gerarSequencia();
  } else {
    display.clear();
    display.drawString(20, 25, "Game Over!");
    display.display();
    delay(1000);
    display.clear();
    tocarMusicaDerrota();

    // Verifica se o jogador atingiu um novo recorde
    if (pontuacao > recorde) 
    {
      recorde = pontuacao; 
      EEPROM.write(enderecoRecorde, recorde); 
      EEPROM.commit(); // Grava o novo recorde na EEPROM
      display.clear();
      display.drawString(15, 15, "Novo Recorde:");
      display.drawString(55, 35, String(recorde));
      display.display();
      delay(1000);
      display.clear();
    } 
    else 
    {
      display.clear();
      display.drawString(10, 15, "Recorde Atual:");
      display.drawString(55, 35, String(recorde));
      display.display();
      delay(1000);
      display.clear();
    }

    // Zera a pontuação do jogador ao perder
    pontuacao = 0;
    esperarNovoJogo();
  }
}

// Função para atualizar a pontuação conforme o nível
void atualizarPontuacao() {
  if (tamanhoSequencia >= 20) {
    pontuacao += 5;  // A partir do nível 20, 5 pontos por acerto
  } else if (tamanhoSequencia >= 11) {
    pontuacao += 2;  // A partir do nível 10, 2 pontos por acerto
  } else {
    pontuacao += 1;  // Até o nível 9, 1 ponto por acerto
  }
}

// Função para calcular o tempo que o LED deve ficar aceso baseado no nível
int calcularTempoLedAceso() {
  if (tamanhoSequencia >= 20) {
    return tempoLedAceso / 2; // A partir do nível 20, LEDs 3 vezes mais rápidos
  } else if (tamanhoSequencia >= 10) {
    return tempoLedAceso / 1.5; // A partir do nível 10, LEDs 2 vezes mais rápidos
  } else {
    return tempoLedAceso; // Até o nível 9, o tempo é o padrão
  }
}

void mostrarSequencia() {
  int tempoAtualLedAceso = calcularTempoLedAceso(); // Calcula o tempo baseado no nível atual
  for (int i = 0; i < tamanhoSequencia; i++) {
    acenderLed(sequencia[i]); 
    delay(tempoAtualLedAceso); // Aguarda pelo tempo ajustado entre o acionamento de cada LED
    apagarLeds();         
    delay(tempoAtualLedAceso); // Pausa ajustada entre cada LED na sequência
  }
}


void acenderLed(int led) {
  switch (led) {
    case 0:
      digitalWrite(ledAmarelo, HIGH);
      tone(buzzer, notaAmarelo);
      break;
    case 1:
      digitalWrite(ledAzul, HIGH);
      tone(buzzer, notaAzul);
      break;
    case 2:
      digitalWrite(ledVerde, HIGH);
      tone(buzzer, notaVerde);
      break;
    case 3:
      digitalWrite(ledVermelho, HIGH);
      tone(buzzer, notaVermelho);
      break;
  }
}

void apagarLeds() {
  digitalWrite(ledAmarelo, LOW);
  digitalWrite(ledAzul, LOW);
  digitalWrite(ledVerde, LOW);
  digitalWrite(ledVermelho, LOW);
  noTone(buzzer);
}

void gerarSequencia() 
{
  sequencia[tamanhoSequencia] = random(4); // Gera um número aleatório entre 0 e 3
  tamanhoSequencia++;
}

void tocarMusicaAbertura() {
  acenderLed(2);
  delay(300);
  apagarLeds();
  delay(100);

  acenderLed(3);
  delay(300);
  apagarLeds();
  delay(100);

  acenderLed(1);
  delay(300);
  apagarLeds();
  delay(100);

  acenderLed(0);
  delay(300);
  apagarLeds();
  delay(1000);  // Espera um segundo antes de iniciar a partida
}

void tocarMusicaDerrota() 
{
int notas[] = {261, 277, 311, 349, 392, 415, 466, 261}; // Notas da escala C Frígio, terminando na tônica
int duracoes[] = {200, 150, 200, 150, 300, 200, 200, 400}; // Duração de cada nota


  for (int i = 0; i < 8; i++) {
    tone(buzzer, notas[i]);
    digitalWrite(ledVermelho, HIGH);
    digitalWrite(ledAmarelo, HIGH);
    digitalWrite(ledAzul, HIGH);
    digitalWrite(ledVerde, HIGH);
    delay(duracoes[i]);
    noTone(buzzer);    
    apagarLeds();
    delay(100); // Pequena pausa entre as notas
  }

  delay(1000); // Espera um segundo antes de permitir iniciar uma nova partida
}

void displayPontuacao() {
  display.setFont(ArialMT_Plain_10);
  display.drawString(75, 0, "Pontos: " + String(pontuacao));
  display.drawString(0, 0, "Record: " + String(recorde));
  display.setFont(ArialMT_Plain_16);
}

void mostrarNivelAtual() {
  display.clear();
  display.drawString(32, 25, "Nível: " + String(tamanhoSequencia));
  displayPontuacao();
  display.display();
  delay(500);
}

bool verificarEntradaJogador() {
  display.clear();
  display.drawString(32, 24, "Sua vez");
  displayPontuacao();
  display.display();
  delay(100);  // Pausa para o jogador se preparar
  display.clear();

  for (int i = 0; i < tamanhoSequencia; i++) {
    int botaoPressionado = esperarBotaoPressionado();
    if (botaoPressionado != sequencia[i]) {
      return false; // Entrada incorreta
    }
  }
  return true; // Entrada correta
}

int esperarBotaoPressionado() {
  while (true) {
    if (digitalRead(botaoAmarelo) == LOW) {
      acenderLed(0);
      delay(tempoLedAceso);
      apagarLeds();
      while (digitalRead(botaoAmarelo) == LOW) {};
      return 0;
    }
    if (digitalRead(botaoAzul) == LOW) {
      acenderLed(1);
      delay(tempoLedAceso);
      apagarLeds();
      while (digitalRead(botaoAzul) == LOW) {};
      return 1;
    }
    if (digitalRead(botaoVerde) == LOW) {
      acenderLed(2);
      delay(tempoLedAceso);
      apagarLeds();
      while (digitalRead(botaoVerde) == LOW) {};
      return 2;
    }
    if (digitalRead(botaoVermelho) == LOW) {
      acenderLed(3);
      delay(tempoLedAceso);
      apagarLeds();
      while (digitalRead(botaoVermelho) == LOW) {};
      return 3;
    }
  }
}

void esperarNovoJogo() {
  display.setFont(ArialMT_Plain_10); // Fonte menor para a pontuação
  display.drawString(0, 0, "Record: " + String(recorde));
  delay(500);
  display.setFont(ArialMT_Plain_16);
  display.drawString(12, 15, "Genius Game");
  display.drawString(20, 35, "Iniciar Jogo");
  display.display();
  display.clear();

  // Criamos uma espera "infinita" por um botão ou todos juntos para resetar
  while (true) {
    if (digitalRead(botaoAmarelo) == LOW && digitalRead(botaoAzul) == LOW &&
        digitalRead(botaoVerde) == LOW && digitalRead(botaoVermelho) == LOW) {
      // Todos os botões pressionados, resetar a EEPROM
      EEPROM.write(enderecoRecorde, 0); // Recorde resetado
      EEPROM.commit();                  // Grava na EEPROM
      recorde = 0;                      // Atualiza o recorde em RAM
      pontuacao = 0;
      tamanhoSequencia = 0;
      display.clear();
      display.drawString(28, 15, "Recorde");
      display.drawString(25, 35, "Resetado");
      display.display();
      delay(2000);
      display.clear();
      esperarNovoJogo(); 
      return; // Sai da função para recomeçar
    }

    // Se um botão for pressionado (iniciar o jogo)
    if (digitalRead(botaoAmarelo) == LOW || digitalRead(botaoAzul) == LOW ||
        digitalRead(botaoVerde) == LOW || digitalRead(botaoVermelho) == LOW) {
      // Aguarda o botão pressionado ser solto.
      while (digitalRead(botaoAmarelo) == LOW || digitalRead(botaoAzul) == LOW ||
             digitalRead(botaoVerde) == LOW || digitalRead(botaoVermelho) == LOW) {};
      tamanhoSequencia = 0; // Reinicia o tamanho da sequência
      gerarSequencia(); // Gera uma nova sequência
      tocarMusicaAbertura(); // Toca a música de abertura com animação de LEDs 
      break;         
    }
  }
}

Video demonstração do projeto do Genius 2.0

Conclusão

O Genius 2.0 com ESP32-C3 é um excelente projeto para quem deseja combinar lógica de programação, controle de hardware e armazenamento de dados. Com o uso da EEPROM para recordes e a interface intuitiva do display OLED, este projeto traz um toque moderno a um clássico jogo de memória. Além de ser divertido, este projeto introduz conceitos importantes de eletrônica e programação, ampliando as possibilidades de criação e desenvolvimento em projetos futuros.