Jogo da Memória


Neste tutorial, vamos fazer um jogo da memória recorrendo ao nosso Arduino. Vamos usar 3 LEDs como indicadores luminosos (e fica, desde já, o desafio para quem quiser adicionar mais LEDs para tornar a tarefa mais complicada!) que deveremos posteriormente replicar no Serial Monitor do Arduino. Esta é a primeira vez que vamos usar o Serial Monitor diretamente embora já tenhamos usado a comunicação série no tutorial da Interface LED com Arduino e Processing. Vê abaixo o vídeo do jogo em ação!

Hardware

Material necessário: 3 LEDs (um verde, um amarelo e um vermelho), 3 resistências de 220 Ohms e alguns fios de montagem.

Para ajudar à montagem, criou-se o seguinte esquema de ligações usando o programa Fritzing.

Acaba por ser exatamente o mesmo esquema do tutorial anterior mas com um fim totalmente diferente.

PRO TIP: Se os teus LEDs tiverem com pouco brilho, isso deve-se ao facto das resistências terem um valor demasiado alto. Podes usar resistências menores ou colocar duas que tenhas em paralelo (a resistência resultante será menor que a menor das duas). Não colocar uma resistência não é recomendado, já que uma pequena variação de tensão se irá traduzir numa grande variação de corrente que poderá fundir o teu LED.

Desenvolvimento do programa

Este jogo envolve vários conceitos/etapas que vamos abordando ao longo do tutorial. A primeira coisa a fazer é definir os nosso três LEDs ligados aos pins 8, 9 e 10 como outputs.

void setup()
{
  // Definir LEDs como OUTPUTs
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
}

Em seguida, precisamos também de iniciar a comunicação série que já sabemos como se faz. Vamos ainda adicionar a seed para o nosso gerador de números aleatórios que vai gerar as diferentes sequências (a mesma função que usámos no “Dado Virtual”; para mais informações consulta esse tutorial). Finalmente, e como o void setup() só corre uma vez, vamos escrever duas linhas de código que irão aparecer no Serial Monitor.

void setup()
{
  // Iniciar comunicacao serie
  Serial.begin(9600);
  // Definir LEDs como OUTPUTs
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  // Definir a seed do gerador de números aleatórios
  randomSeed(analogRead(0));
}

Pela primeira vez, vamos fazer um print que é algo muito comum em programação. Fazer print significa escrever algo na consola para que o utilizador saiba o que fazer a seguir. Para fazermos um print para o Serial Monitor usamos o método Serial.print() ou Serial.println(). A diferença entre os dois assenta basicamente no facto deste último método fazer uma quebra de linha entre cada linha. Se usássemos o Serial.print() a informação iria aparecer toda lado a lado sem qualquer tipo de espaçamento entre ela.

Vamos então informar o jogador que o jogo começou e o que ele precisa de fazer para começar a jogar.

void setup()
{
  // Iniciar comunicacao serie
  Serial.begin(9600);
  // Definir LEDs como OUTPUTs
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  // Definir a seed do gerador de números aleatórios
  randomSeed(analogRead(0));
  // Dar as instrucoes de inicio de jogo ao jogador
  Serial.println("**** INICIO DO JOGO ****");
  Serial.println("Prime s para comecar");
}

Assim, quando corrermos o nosso programa e abrirmos o Serial Monitor, estas duas linhas de texto irão aparecer.

Início de cada nível

O nosso Jogo da Memória será dividido em 5 níveis diferentes cuja dificuldade vai aumentando substancialmente. O jogador sabe em que nível está uma vez que também isso será mostrado na consola. No entanto, deve ser feito um aviso de quando a sequência está pronta a ser mostrada. Uma boa alternativa é piscar todos os LEDs duas vezes, esperar um segundo e mostrar a sequência. Assim, o jogador nunca é apanhado desprevenido. Para facilitar as coisas, vamos criar uma função void blinkTwice() que faz exatamente o que o nome diz: faz com que todos os LEDs pisquem duas vezes. Ora vocês já consequem claramente escrever este tipo de função. Escreve-a e depois podes confirmar abaixo (mas não sem tentares primeiro!) a solução.

void blinkTwice()
{
  // Ligar todos os LEDs
  digitalWrite(8, HIGH);
  digitalWrite(9, HIGH);
  digitalWrite(10, HIGH);
  //Mantê-los ligados 1 segundo
  delay(1000);
  // Desligar todos os LEDs
  digitalWrite(8, LOW);
  digitalWrite(9, LOW);
  digitalWrite(10, LOW);
  // Mantê-los desligados 1 segundo
  delay(1000);
  // Ligar todo os LEDs
  digitalWrite(8, HIGH);
  digitalWrite(9, HIGH);
  digitalWrite(10, HIGH);
  // Mantê-los ligados 1 segundo
  delay(1000);
  // Desligar todos os LEDs
  digitalWrite(8, LOW);
  digitalWrite(9, LOW);
  digitalWrite(10, LOW);
}

Como podemos ver, este código é redundante (repete-se desnecessariamente). No fim deste tutorial, já terão as ferramentas necessárias para escrever esta função de um modo mais compacto.

As outras instruções serão dadas no void loop() que será a última coisa a ser escrita já que vai juntar todas as componentes do jogo.

Gerar sequência aleatória

O nosso jogo vai gerar uma sequência aleatória que a cada número gerado tem um LED associado. A ordem proposta é a que se encontra na imagem abaixo.

Para gerarmos a nossa sequência aleatória vamos definir uma função só para este efeito. Neste caso, é a função void generateSequence (int tempo, int sequencia). A primeira coisa a fazer é chamar a função blinkTwice() e dar um delay de 1 segundo antes da sequência começar.

void generateSequence (int tempo, int sequencia)
{
  // Piscar os LEDs todos duas vezes
  blinkTwice();
  // Esperar 1 segundo para que o jogo comece
  delay(1000);
 }

Como podemos ver, esta função gera uma sequência baseada em dois parâmetros: o tempo que cada LED está aceso e o número de LEDs acesos que a sequência tem. Antes de gerarmos os nossos números aleatoriamente, precisamos de um sítio para os guardar. Para isso, usamos um tipo de estrutura dado conhecida como lista. Em C++ (que é a linguagem em que o Arduino IDE é baseado) precisamos sempre de definir o tamanho de uma lista. Por esta razão é que um dos argumentos da nossa função (int sequencia) é o tamanho da sequência. Assim, não precisamos de reescrever uma função sempre que queremos adicionar mais elementos à nossa sequência.

Podemos ter uma lista de vários tipos de variáveis. No nosso caso, será uma lista que guarda inteiros. Não nos devemos esquecer que as listas são naturalmente indexadas a zero e que ter em conta os limites da nossa lista é essencial. Se prestarmos atenção, nós já conhecemos o conceito de lista intuitivamente. Uma String é uma lista de caracteres (char) que podemos chamar usando os respetivos índices tal como fazemos numa lista.

  // Criar uma lista de inteiros com o tamanho que e passado como argumento
  int ordemLeds[sequencia];

O int desta linha refere-se ao facto de ser uma lista de inteiros. ordemLeds é simplesmente o nome que foi dado à lista e sequencia é o tamanho da lista. Geralmente, é um número mas neste caso é uma variável que remete para um inteiro.

Agora que já temos um sítio para guardar a sequência de números, podemos gerá-la. Queremos gerar tantos números quantos os indicados na variável sequência. Para preenchermos a lista, usamos um ciclo for. Um ciclo foré útil quando pretendemos repetir uma dada instrução um determinado número de vezes.

Quando usamos um ciclo for, precisamos de inicializar uma variável que vai contar o número de iterações e definir o seu valor mínimo e o seu valor máximo para o qual o ciclo deixa de correr. Temos ainda de dizer se queremos que a variável aumente ou diminua a cada iteração.

  // Gerar sequencia aleatoria
  for (int i = 0; i < sequencia; i++)
  {
    numeroGerado = random(1, 4);
    ordemLeds[i] = numeroGerado;
  }

É quase uma convenção usar o i como a variável que vai contar as iterações. Inicializamo-la a 0 na primeira condição do ciclo. Na segunda condição, definimos o valor máximo da variável. Atenção que se trata de um menor e não de um menor ou igual. Por exemplo, se sequencia == 3 então i irá tomar os seguintes valores por ordem: 0, 1, 2. O ciclo itera 3 vezes. A terceira condição do ciclo diz-nos se a variável incrementa (i++) ou decrementa (i–). Esta notação significa que i++ == i + 1 e i-- == i - 1.

Portanto, a cada iteração do nosso ciclo, um número aleatório é gerado entre 1 e 3. Lembra-te que o método random() tem dois argumentos quando queremos gerar números num dado intervalo. Como queremos gerar números entre 1 e 3, passamos como argumentos 1 e 4 random(min inclusive, max exclusive).

Depois de gerado o número, guardamo-lo na lista que criámos anteriormente. Agora que a sequência aleatória já foi gerada e guardada, temos de pôr os nossos LEDs a piscar de acordo com ela. Para isso, usamos novamente um ciclo for. Consegues pensar como podemos fazer isto?

As três condições são iguais às do último ciclo com a exceção de que substituímos o nome da variável. Começamos a ler a lista começando no índice 0. Se este valor for igual a 1, ligamos o primeiro LED; se for igual a 2, ligamos o segundo LED e assim sucessivamente. Usamos três condições do tipo if para chegar a este resultado.

// Piscar os LEDs na sequencia gerada
  for (int j = 0; j < sequencia; j++)
  {
    led = ordemLeds[j];

    if (led == 1)
    {
      digitalWrite(8, HIGH);
      delay(tempo);
      digitalWrite(8, LOW);
      delay(tempo);
    }

    else if (led == 2)
    {
      digitalWrite(9, HIGH);
      delay(tempo);
      digitalWrite(9, LOW);
      delay(tempo);
    }

    else if (led == 3)
    {
      digitalWrite(10, HIGH);
      delay(tempo);
      digitalWrite(10, LOW);
      delay(tempo);
    }

A linha led = ordemLeds[j]; usa uma variável chamada led para guardar o elemento da lista de índice j. Isto não é estritamente necessário para torna o código mais fácil de escrever. A variável led é um int que deve ser inicializado no início do código com a instrução int led.

Como deves ter reparado, não fechámos o nosso ciclo for. A última coisa a fazer é converter a nossa lista numa String (o porquê desta decisão será claro em breve). Para isso, inicializámos uma String também no início do código chamada sequenciaNumerica.

Para fazer esta conversão, concatenamos a String sequenciaNumerica, ou seja, adicionamos os elementos à String por ordem. Assim, depois do último else if () adicionamos a linha sequenciaNumerica = sequenciaNumerica + led;. Para percebermos melhor o que está a acontecer, vamos ter em conta que na primeira iteração, sequenciaNumerica == "", ou seja, trata-se de uma String vazia.

Quando chegamos ao fim da primeira iteração do ciclo for, juntamos o primeiro elemento da nossa lista à nossa String. Por exemplo, se o primeiro índice for 1, passamos de sequenciaNumerica = "" para sequenciaNumerica == "1".

Falta apenas um pequeno pormenor para encerrarmos o método generateSequence(). Ainda não estabelecemos que a String inicialmente é uma String vazia. Para isso, adicionamos a linha sequenciaNumerica = ""; antes do último ciclo for.

Posto isto, o nosso método generateSequence completo é então:

void generateSequence (int tempo, int sequencia)
{
  // Piscar os LEDs todos duas vezes
  blinkTwice();
  // Esperar 1 segundo para que o jogo comece
  delay(1000);

  // Criar uma lista de inteiros com o tamanho que e passado como argumento
  int ordemLeds[sequencia];

  // Gerar sequencia aleatoria
  for (int i = 0; i < sequencia; i++)
  {
    numeroGerado = random(1, 4);
    ordemLeds[i] = numeroGerado;
  }

  // Inicialmente, a String sequenciaNumerica é uma String vazia
  sequenciaNumerica = "";

  // Piscar os LEDs na sequencia gerada
  for (int j = 0; j < sequencia; j++)
  {
    led = ordemLeds[j];

    if (led == 1)
    {
      digitalWrite(8, HIGH);
      delay(tempo);
      digitalWrite(8, LOW);
      delay(tempo);
    }

    else if (led == 2)
    {
      digitalWrite(9, HIGH);
      delay(tempo);
      digitalWrite(9, LOW);
      delay(tempo);
    }

    else if (led == 3)
    {
      digitalWrite(10, HIGH);
      delay(tempo);
      digitalWrite(10, LOW);
      delay(tempo);
    }
    // Converter a lista numa String   
    sequenciaNumerica = sequenciaNumerica + led;
  }
}

Validar a resposta do jogador

De seguida, precisamos de criar um método que valide a resposta do jogador. A melhor maneira de expressar a vitória ou a derrota num jogo é uma variável boolean. Tem o valor true se o jogador introduziu a resposta certa e o valor false se o jogador introduzir a resposta errada.

Começamos por inicializar uma variável win do tipo boolean cujo resultado o método vai devolver. O seu valor por defeito é true. A razão pela qual lhe damos um valor inicial é porque se este valor estivesse totalmente dependente de uma condição if(), o método daria erro já que a condição poderia nunca se concretizar e não haveria nenhum valor para devolver.

Em seguida, fazemos um print para o Serial Monitor para que o jogador saiba que já pode introduzir a sua resposta. Até agora, temos:

boolean checkUserInput()
{
  // Variável que guarda o resultado do jogo
  boolean win = true;

  Serial.println("**** Insere a tua resposta ****");

Para esperarmos por uma resposta do utilizador, usamos uma condição if vazia que apenas é validade quando o utilizador insere a sua resposta. O método Serial.available() é diferente de 0 quando o jogador introduzir com o Serial Monitor.

  // Aguardar a resposta do utilizador
  while (Serial.available() == 0)
  {}

Depois do jogador introduzir a sua resposta, vamos lê-la e guarda-la numa String user, inicializada no início do nosso programa. Esta é a razão pela qual no método que gera a sequência, convertemos a lista numa String.

  // Guardar o valor introduzido pelo utilizador no Serial Monitor numa String user
  if (Serial.available())
  {
    user = Serial.readString();
  }

Agora que já temos a resposta do jogador guardada na String user, basta compararmos com a String sequenciaNumerica que gerámos. Para isso usamos o método equals() que compara Strings e devolve true se elas forem iguais. Se elas forem diferentes, win passa a false. Caso contrário, mantém-se inalterada como true. Depois, basta fazer return win;.

  // Se as sequências não coincidirem, a variável win passa a false
  if (!user.equals(sequenciaNumerica))
  {
    win = false;
  }

  return win;

A condição do if() pode parecer um pouco estranha mas é apenas uma maneira de escrever as mesmas coisas de uma forma mais condensada. O ponto de exclamação antes de invocarmos o método, nega a condição. Isto é precisamente o mesmo que ter:

  user.equals(sequenciaNumerica) != true;

onde != é o operador diferente de.

Juntar tudo!

O mais difícil já está feito. Agora só temos de juntar tudo no void loop().Para iniciar o jogo, vamos exigir que o jogador envie o caracter ‘s’ através do Serial Monitor. Isto é o equivalente ao “Press Start” que vemos muitas vezes nos jogos. Isto não é feito ao acaso: assim, o jogo só começa quando o jogador estiver preparado e tiver aberto o Serial Monitor.

  void loop() {
  if (Serial.available())
  {
    if (Serial.reads() == 's')
    {

    }
  }
}

Agora podemos começar a juntar os níveis e podemos fazer as combinações de tempo entre os LEDs acenderem e do tamanho da sequência que quisermos. Podes tornar o jogo o mais fácil ou o mais difícil que quiseres. O jogo deste tutorial tem 5 níveis com as seguintes caraterísticas.

Nível Tempo (em segundos) Tamanho da sequência
1 1 3
2 0.5 3
3 1 4
4 0.5 4
5 0.5 5

Como construímos os níveis? Basicamente, usando condições if encadeadas.

if (Serial.available())
  {
    if (Serial.read() == 's')
    {
      Serial.println("----- NIVEL 1 -----");
      generateSequence(1000, 3);

      if (checkUserInput())
      {
        Serial.println("CORRETO!");
        Serial.println("----- NIVEL 2 -----");
        generateSequence(500, 3);

      else
      {
        Serial.println("GAME OVER :(");
      }  

Fazemos isto sucessivamente até termos tantos níveis quanto os desejados. Podes fazer imensas combinações diferentes e adicionar mais LEDs para tornares o jogo mais desafiante!

Código Completo

long numeroGerado;
int led;
String sequenciaNumerica = "";
String user;
String copia;

void setup() {
  // Iniciar comunicacao serie
  Serial.begin(9600);
  // Definir LEDs como OUTPUTs
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  // Definir a seed do gerador de números aleatórios
  randomSeed(analogRead(0));
  // Dar as instrucoes de inicio de jogo ao jogador
  Serial.println("**** INICIO DO JOGO ****");
  Serial.println("Prime s para comecar");
}

void loop() {
  if (Serial.available())
  {
    if (Serial.read() == 's')
    {
      Serial.println("----- NIVEL 1 -----");
      generateSequence(1000, 3);

      if (checkUserInput())
      {
        Serial.println("CORRETO!");
        Serial.println("----- NIVEL 2 -----");
        generateSequence(500, 3);

        if (checkUserInput())
        {
          Serial.println("CORRETO!");
          Serial.println("----- NIVEL 3 -----");
          generateSequence(1000, 4);

          if (checkUserInput())
          {
            Serial.println("CORRETO!");
            Serial.println("----- NIVEL 4 -----");
            generateSequence(500, 4);

            if (checkUserInput())
            {
              Serial.println("CORRETO!");
              Serial.println("----- NIVEL 5 -----");
              generateSequence(500, 5);

              if (checkUserInput())
              {
                Serial.println("CORRETO!");
                Serial.println("PARABÉNS! GANHASTE O JOGO!");
              }

              else
              {
                Serial.println("GAME OVER :(");
              }
            }

            else
            {
              Serial.println("GAME OVER :(");
            }
          }

          else
          {
            Serial.println("GAME OVER :(");
          }
        }

        else
        {
          Serial.println("GAME OVER :(");
        }
      }

      else
      {
        Serial.println("GAME OVER :(");
      }
    }
  }
}

void generateSequence (int tempo, int sequencia)
{
  // Piscar os LEDs todos duas vezes
  blinkTwice();
  // Esperar 1 segundo para que o jogo comece
  delay(1000);

  // Criar uma lista de inteiros com o tamanho que e passado como argumento
  int ordemLeds[sequencia];

  // Gerar sequencia aleatoria
  for (int i = 0; i < sequencia; i++)
  {
    numeroGerado = random(1, 4);
    ordemLeds[i] = numeroGerado;
  }

  // Inicialmente, a String sequenciaNumerica é uma String vazia
  sequenciaNumerica = "";

  // Piscar os LEDs na sequencia gerada
  for (int j = 0; j < sequencia; j++)
  {
    led = ordemLeds[j];

    if (led == 1)
    {
      digitalWrite(8, HIGH);
      delay(tempo);
      digitalWrite(8, LOW);
      delay(tempo);
    }

    else if (led == 2)
    {
      digitalWrite(9, HIGH);
      delay(tempo);
      digitalWrite(9, LOW);
      delay(tempo);
    }

    else if (led == 3)
    {
      digitalWrite(10, HIGH);
      delay(tempo);
      digitalWrite(10, LOW);
      delay(tempo);
    }
    // Converter a lista numa String   
    sequenciaNumerica = sequenciaNumerica + led;
  }
}

boolean checkUserInput()
{
  // Variável que guarda o resultado do jogo
  boolean win = true;

  Serial.println("**** Insere a tua resposta ****");

  // Aguardar a resposta do utilizador
  while (Serial.available() == 0)
  {}

  // Guardar o valor introduzido pelo utilizador no Serial Monitor numa String user
  if (Serial.available())
  {
    user = Serial.readString();
  }

  // Se as sequências não coincidirem, a variável win passa a false
  if (!user.equals(sequenciaNumerica))
  {
    win = false;
  }

  return win;
}


void blinkTwice()
{
  // Ligar todos os LEDs
  digitalWrite(8, HIGH);
  digitalWrite(9, HIGH);
  digitalWrite(10, HIGH);
  //Mantê-los ligados 1 segundo
  delay(1000);
  // Desligar todos os LEDs
  digitalWrite(8, LOW);
  digitalWrite(9, LOW);
  digitalWrite(10, LOW);
  // Mantê-los desligados 1 segundo
  delay(1000);
  // Ligar todo os LEDs
  digitalWrite(8, HIGH);
  digitalWrite(9, HIGH);
  digitalWrite(10, HIGH);
  // Mantê-los ligados 1 segundo
  delay(1000);
  // Desligar todos os LEDs
  digitalWrite(8, LOW);
  digitalWrite(9, LOW);
  digitalWrite(10, LOW);
}

Deixa um comentário