Projetos

Jogo da Velha na TV!

Eletrogate 19 de setembro de 2023

Materiais Necessários para o Projeto Jogo da Velha na TV

Para este tutorial, você vai precisar dos seguintes componentes eletrônicos:


Introdução

Este é um jogo de quebra-cabeça para dois jogadores, identificados como “X” e “O”, que se revezam marcando os espaços em uma área 3 × 3.
Alguma vez na vida você já deve ter jogado este clássico jogo conhecido como Jogo-da-Velha ou Tic-Tac-Toe.

Neste post, você vai aprender como montar e programar uma versão digital deste jogo, com saída de imagem para TV, utilizando apenas alguns componentes eletrônicos básicos, uma placa Arduino UNO e um Teclado Matricial de 16 teclas.


Biblioteca Gráfica TVout

O videogame construído neste projeto gera imagens para TV. Portanto, estaremos utilizando a biblioteca TVout para a plataforma Arduino.

A biblioteca TVout permite a geração de um sinal de imagem de vídeo composto no padrão NTSC ou PAL, a uma resolução de 128×96 píxeis. Esta resolução pode ser definida para um valor diferente, maior ou menor, dependendo dos recursos de memória da placa Arduino que for utilizada.

TVout pode ser utilizada em diferentes modelos de placas Arduino, mas, dependendo do modelo da placa, as conexões de saída de áudio e vídeo podem ser diferentes. Neste projeto, estaremos utilizando uma placa Arduino UNO.

Instalação

Para instalar a biblioteca TVout, acesse o menu Sketch ► Incluir Biblioteca ► Gerenciar Bibliotecas…

Na tela do Gerenciador de Bibliotecas procure pelo nome “TVout” e clique no botão INSTALAR.
A biblioteca será instalada dentro deste caminho: “\Documents\Arduino\libraries\TVout

IMPORTANTE: Assim que a instalação terminar, abra o windows explorer e acesse o local onde foi instalada a biblioteca.
Como você pode observar, a pasta “TVoutfonts” se encontra dentro da pasta “TVout” e, desta forma, o Arduino IDE não vai conseguir encontrá-la para compilar o código.
Para corrigir isto, você precisa recortar esta pasta “TVoutfonts” e colocá-la em um nível anterior, ou seja, dentro da pasta “libraries”, de forma que, quando você estiver dentro da pasta Libraries, você possa ver tanto a pasta “TVout” quanto a pasta “TVoutfonts”.


Hardware

Para o desenvolvimento deste projeto siga o diagrama abaixo:

(Abra a imagem em uma nova guia para visualizar com maior resolução)

O hardware é composto de duas partes: a primeira é utilizada pela biblioteca TVout para a emissão do sinal de vídeo, a segunda é utilizada pelo jogo, propriamente dito, para receber os valores numéricos informados pelos jogadores através de um Teclado Matricial.

Saída de Imagem para a TV

A biblioteca TVout utiliza dois resistores para a composição de seu sinal de vídeo, um resistor de 1k Ohms e outro de 470 Ohms.

Entrada de Dados

Para que o jogo saiba que posição ele deve marcar no tabuleiro, ele utiliza as teclas de 1 a 9 de um Teclado Matricial 4×4 de 16 teclas, que fica conectado às portas digitais 2, 3, 4, 10, 11 e 12.


Funcionamento

O fornecimento de energia do videogame deve ser de 5V.
Você pode conectar a Arduino UNO a um cabo USB e ligar este cabo à porta USB do computador ou a um carregador de celular.

O jogo já inicia exibindo o tabuleiro vazio e, ao lado direito da tela, mostra de quem é a vez de jogar.
O programa então fica aguardando que o jogador aperte alguma das teclas para poder marcar o seu respectivo ícone no tabuleiro, na posição indicada.

Assim que a posição do jogador é marcada no tabuleiro ele alterna a vez para o outro jogador e aguarda que ele também aperte uma das teclas para poder marcar seu ícone no tabuleiro.

E assim o jogo vai seguindo, alternando entre os jogadores, até que um deles consiga fazer uma sequência de marcações na horizontal, na vertical ou na diagonal.
Se o tabuleiro for completamente preenchido e não houver nenhum vencedor, então o jogo declara que houve um empate.

Se houver um vencedor o jogo irá traçar uma linha através da sequência. Após alguns segundos o jogo irá reiniciar automaticamente.
Se houver um empate o jogo irá escrever na tela a palavra “EMPATE” e, após alguns segundos, irá reiniciar automaticamente.

Vídeo demonstrando o funcionamento do Jogo da Velha!

Software

Para fazer o download do código do jogo, clique neste link: Eletrogate JogoDaVelha.zip

Instalação
  • Extraia o conteúdo do arquivo .ZIP e abra o arquivo “Eletrogate_JogoDaVelha.ino” através do Arduino IDE,
  • Conecte a Arduino UNO no computador através de um cabo USB,
  • Acesse o menu Ferramentas ► Placa ► Arduino AVR Boards e escolha “Arduino UNO“,
  • Clique em Sketch ► Carregar ou use o atalho Ctrl + U.
Ligando o seu Jogo da Velha na TV

Assim que você já tiver montado o circuito e enviado o código do jogo para sua Arduino, você já pode começar a jogar:

  • Conecte a saída de vídeo no conector AV / RCA amarelo da TV,
  • Conecte a Arduino a um cabo USB e ligue a outra ponta deste cabo a um carregador de celular.
Entendendo a lógica de funcionamento…

O projeto Jogo da Velha na TV é composto por 2 arquivos:
“Eletrogate_JogoDaVelha.ino” e “tecladoMatricial.h”

A programação do jogo é bem simples de entender. Vamos conferir o funcionamento de cada uma das partes através da explicação que segue logo abaixo:

arquivo “Eletrogate_JogoDaVelha.ino”
Parte 1 – bibliotecas e variáveis iniciais
‌‌‌
#include <TVout.h>
#include <fontALL.h>
#include "TecladoMatricial.h"

TVout tv;
int marcaAtual;
int marcas[9];
bool fimDeJogo;‌‌

Inicialmente incluímos as bibliotecas TVout e fontALL para podermos utilizar os recursos gráficos e os recursos de texto da biblioteca TVout.
Incluímos também o arquivo “TecladoMatricial.h”, que possui as funções necessárias para nos informar qual tecla foi pressionada no teclado matricial.

Em seguida instanciamos algumas variáveis globais importantes que vamos utilizar em diversas parte do código principal.

Declaramos um objeto tv da classe TVout para utilizarmos suas principais funcionalidades.

Definimos a variável marcaAtual para indicar qual será o tipo de marca a ser inserida na matriz assim que o jogador apertar uma tecla numérica.
–  O valor 0 indica a marca de círculo → “O“,
–  O valor 1 indica a marca de xis → “X“.

Definimos a variável marcas como uma matriz de 9 índices, que irá armazenar os valores correspondentes de X e O à medida que os jogadores forem fazendo suas marcações.
–  O valor -1 indica que não há marcação na posição,
–  O valor 0 indica a marca de círculo,
–  O valor  1 indica a marca de xis.

Definimos a variável fimDeJogo que será sinalizada como Verdadeiro sempre que o jogo terminar, ou seja, sempre que ocorrer uma vitória ou um empate.

Parte 2 – SETUP
‌
void setup()
{
  tv.begin(NTSC, 128,90);
  iniciarTecladoMatricial(12,11,10,  4,3,2);
  reiniciarJogo();
}

A função SETUP é executada uma única vez assim que a placa Arduino é ligada e começa a executar sua programação. Dentro da função SETUP nós temos a seguinte lógica:

Executamos o método tv.begin(NTSC, 128,90) para iniciar o funcionamento da biblioteca TVout no padrão NTSC com uma resolução gráfica de 128 x 90 pixels.

Executamos a função iniciarTecladoMatricial, que pertence ao arquivo “tecladoMatricial.h”, para que as configurações iniciais das portas digitais sejam realizadas.

Executamos a função reiniciarJogo para resetar os valores de algumas variáveis deixando-as prontas para que seja iniciada uma nova partida.

Parte 3 – reiniciarJogo
‌
void reiniciarJogo()
{
  fimDeJogo = false;
  marcaAtual = 1;
  
  for (int i = 0; i < 9; i++) {
    marcas[i] = -1;
  }
}‌

Sempre que uma partida se encerra, havendo um vencedor ou um empate, o jogo deverá ser reiniciado. E neste momento a função reiniciarJogo é executada.

Esta função coloca a variável fimDeJogo com o valor False para indicar que, já que o jogo reiniciou, ainda não há nem vitória e nem empate; coloca a variável marcaAtual como 1, para indicar que é a vez do ‘Jogador X’ jogar; e também coloca o valor -1 em todos os índices da matriz marcas, para indicar que não há nem X e nem O em nenhuma das posições.

Parte 4 – LOOP
‌
int last_pos, pos;

void loop()
{
  last_pos = pos;
  pos = lerTecladoMatricial() - 1;

  if ((marcas[pos] == -1) && (pos >= 0) && (pos != last_pos))
  {
    marcas[pos] = marcaAtual;
    marcaAtual = 1 - marcaAtual;
    tv.delay(50);
  }

  desenharTabuleiro();

  conferirVitoriaOuEmpate();

  if (fimDeJogo == false) {
    tv.select_font(font6x8);
    tv.print(86,5, "Jogador");

    tv.select_font(font8x8ext);
    if(marcaAtual == 0) {
      tv.print(102,18, "O");
    } else
    if(marcaAtual == 1) {
      tv.print(102,18, "X");
    }
  }
  
  tv.delay_frame(1);
}‌

A função LOOP é uma função que é executada constantemente. Ela é executada logo após a execução da função SETUP.
Aqui dentro da função LOOP nós teremos a seguinte lógica:

A variável pos chama a função lerTecladoMatricial, que pertence ao arquivo “tecladoMatricial.h”, para saber qual tecla foi pressionada.
OBS: Removemos 1 do valor retornado pela função para que o valor obtido seja equivalente ao seu respectivo índice na matriz.

Logo em seguida o programa verifica se a posição da matriz marcas, correspondente à tecla pressionada, está com o valor -1 (este valor indica que nesta posição ainda não há nem X ou O).
Se sim, o programa coloca o valor da variável marcaAtual nesta posição da matriz.

A seguir a função desenharTabuleiro é chamada para executar todos os comandos responsáveis por desenhar visualmente o tabuleiro e as marcações.

Depois o programa chama a função conferirVitoriaOuEmpate para verificar se houve um ganhador ou se o jogo deu empate, para determinar se ocorreu o fim do jogo.

E por fim, se a variável fimDeJogo ainda estiver como False, o programa escreve a mensagem de qual jogador deve jogar no momento: “Jogador X” ou “Jogador O”.

Parte 5 – desenharTabuleiro
‌
void desenharTabuleiro()
{
  int x, y;

  tv.clear_screen();
  tv.draw_rect(28,5, 1, 79, 1);
  tv.draw_rect(55,5, 1, 79, 1);
  tv.draw_rect(3,30, 79, 1, 1);
  tv.draw_rect(3,57, 79, 1, 1);

  for (int i = 0; i < 9; i++)
  {
    // Seta a posição X,Y da marca a ser desenhada
    
    // posição da linha 1
    x = 7 + 27*i;
    y = 9;

    // posição da linha 2
    if (i > 2) {
      x = 7 + 27*i - 27*3;
      y = 9 + 27*1;
    }

    // posição da linha 3
    if (i > 5) {
      x = 7 + 27*i - 27*6;
      y = 9 + 27*2;
    }

    // Desenha a marca na posição X,Y determinada
    if (marcas[i] == 0) {
      // desenha a marca 'O'
      tv.draw_circle(x+8, y+8, 8, 1);
    } else
    if (marcas[i] == 1) {
      // desenha a marca 'X'
      tv.draw_line(x+1, y+1,  x+15,y+15, 1);
      tv.draw_line(x+1, y+15, x+15,y+1,  1);
    }
  }
}‌

Esta função começa já começa limpando toda a tela, e depois desenha as linhas horizontais e verticais do tabuleiro do jogo da velha.

Depois ela percorre todos os índices da matriz marcas e calcula a posição X e Y correspondente à sua respectiva posição no tabuleiro.
Se o valor daquele índice da matriz for igual a zero ele vai desenhar o círculo e se o valor for igual a um ele vai desenhar o xis na posição que foi calculada. Se o valor for igual a -1 ele simplesmente ignora e não desenha nada.

Parte 6 – conferirVitoriaOuEmpate
‌
void conferirVitoriaOuEmpate()
{
  int vencedor = -1;  // 0 = Empate,  1 = houve vencedor
  int tempoPausa = 800;
  
  // verificar COLUNAS
  if ((marcas[0] >= 0) && (marcas[0] == marcas[3]) && (marcas[0] == marcas[6])) {
    tv.delay(tempoPausa);
    int x = 15 + 27*0;
    tv.draw_rect(x-1,6, 2, 79, 0,1);
    vencedor = 1;
  } else
  if ((marcas[1] >= 0) &&(marcas[1] == marcas[4]) && (marcas[1] == marcas[7])) {
    tv.delay(tempoPausa);
    int x = 15 + 27*1;
    tv.draw_rect(x-1,6, 2, 79, 0,1);
    vencedor = 1;
  } else
  if ((marcas[2] >= 0) &&(marcas[2] == marcas[5]) && (marcas[2] == marcas[8])) {
    tv.delay(tempoPausa);
    int x = 15 + 27*2;
    tv.draw_rect(x-1,6, 2, 79, 0,1);
    vencedor = 1;
  }

  // verificar LINHAS
  if ((marcas[0] >= 0) && (marcas[0] == marcas[1]) && (marcas[0] == marcas[2])) {
    tv.delay(tempoPausa);
    int y = 17 + 27*0;
    tv.draw_rect(4,y-1, 79,2, 0,1);
    vencedor = 1;
  } else
  if ((marcas[3] >= 0) && (marcas[3] == marcas[4]) && (marcas[3] == marcas[5])) {
    tv.delay(tempoPausa);
    int y = 17 + 27*1;
    tv.draw_rect(4,y-1, 79,2, 0,1);
    vencedor = 1;
  } else
  if ((marcas[6] >= 0) && (marcas[6] == marcas[7]) && (marcas[6] == marcas[8])) {
    tv.delay(tempoPausa);
    int y = 17 + 27*2;
    tv.draw_rect(4,y-1, 79,2, 0,1);
    vencedor = 1;
  }

  // Verificar DIAGONAIS
  if ((marcas[0] >= 0) && (marcas[0] == marcas[4]) && (marcas[0] == marcas[8])) {
    tv.delay(tempoPausa);
    tv.draw_line(6,8-2, 78,80-2, 0);
    tv.draw_line(6,8, 78,80, 1);
    tv.draw_line(6,8+2, 78,80+2, 0);
    vencedor = 1;
  } else
  if ((marcas[2] >= 0) && (marcas[2] == marcas[4]) && (marcas[2] == marcas[6])) {
    tv.delay(tempoPausa);
    tv.draw_line(6,80-2, 78,8-2, 0);
    tv.draw_line(6,80, 78,8, 1);
    tv.draw_line(6,80+2, 78,8+2, 0);
    vencedor = 1;
  }

  // Verifica se houve um EMPATE
  bool matrizPreenchida = true;
  for (int i = 0; i < 9; i++) {
    if(marcas[i] == -1) {
      matrizPreenchida = false;
      break;
    }
  }

  // Declara que houve um EMPATE
  if(matrizPreenchida && (vencedor == -1)) {
    tv.delay(tempoPausa);
    vencedor = 0;
    tv.select_font(font6x8);
    tv.print(90,40, "EMPATE");
    tv.delay(2000);
  }

  // Se houve um vencedor ou se ocorreu um empate, então é considerado Fim-De-Jogo.
  fimDeJogo = (vencedor >= 0);

  // Aguarda alguns segundos, limpa a tela e reinicia a partida.
  if(fimDeJogo) {
    tv.delay(8000);
    tv.clear_screen();
    tv.delay(tempoPausa);
    reiniciarJogo();
  }
  
}‌

Esta função verifica se houve um vencedor ou se a partida terminou em um empate.

Primeiro ele verifica na matriz marcas se alguma das 3 colunas possui uma sequência na vertical.
Depois ele verifica se alguma das 3 linhas possui uma sequência na horizontal.
E, por último, ele verifica se sequências nas diagonais.

Se houver uma sequência igual na horizontal, na vertical ou na diagonal, o jogo faz um risco indicando tal sequência e, após alguns segundos, a partida é reiniciada.

Logo após fazer estas verificações, se ainda não houver nenhum vencedor, ele confere se a matriz foi totalmente preenchida com ‘0’ ou ‘1’; Se sim, então ele declara que houve um empate e escreve o texto “EMPATE” na tela. Após alguns segundos a partida é reiniciada.

Se houver um vencedor ou se ocorrer um EMPATE, então a variável fimDeJogo recebe o valor True para que o programa saiba que o jogo já terminou e que não é mais necessário escrever o texto “Jogador X” ou “Jogador O” na tela.

arquivo “tecladoMatricial.h”
Parte 1 – variáveis globais
‌‌‌
int L1, L2, L3;
int C1, C2, C3;

Inicialmente declaramos as variáveis globais C1, C2, C3, L1, L2 e L3 para receber os valores das portas digitais que serão atribuídos através da função iniciarTecladoMatricial.
Sendo variáveis globais elas poderão ser acessadas a partir de qualquer função em nosso programa.

Parte 2 – iniciarTecladoMatricial
‏‏
void iniciarTecladoMatricial(int col1, int col2, int col3, int lin1, int lin2, int lin3)
{
  // Atribui às variáveis globais os valores recebidos por parâmetro.
  L1 = lin1;
  L2 = lin2;
  L3 = lin3;
  C1 = col1;
  C2 = col2;
  C3 = col3;

  // Define como OUTPUT as portas onde estão conectadas às colunas
  pinMode(col1, OUTPUT);
  pinMode(col2, OUTPUT);
  pinMode(col3, OUTPUT);

  // Define como INPUT_PULLUP as portas onde estão conectadas às linhas
  pinMode(lin1, INPUT_PULLUP);
  pinMode(lin2, INPUT_PULLUP);
  pinMode(lin3, INPUT_PULLUP);
}

Esta função configura as portas digitais do Arduino UNO como OUTPUT para as colunas, e como INPUT_PULLUP para a linhas.

Ela recebe como parâmetros as variáveis col1, col2, col3, lin1, lin2 e lin3 e atribui seus valores às suas respectivas variáveis globais C1, C2, C3, L1, L2 e L3.

As portas digitais atribuídas às colunas são definidas como OUTPUT. Desta forma podemos definir em que momento queremos que estas portas se comportem como um pino GND ou como um pino +5V simplesmente escrevendo valores False ou True nelas.

As portas digitais atribuídas às linhas são definidas como INPUT, mais especificamente INPUT_PULLUP. Isto faz com que as portas sejam definidas como portas de entrada e, ao mesmo tempo, sejam conectadas internamente a um resistor de PULL-UP, fazendo com que elas fiquem constantemente em nível lógico ALTO, quando nenhum botão estiver sendo pressionado no teclado, evitando possíveis variações incorretas de valores.

Parte 3 – lerTecladoMatricial
‏‏
int lerTecladoMatricial()
{
  int b = -1;
  
  digitalWrite(C1, HIGH);
  digitalWrite(C2, HIGH);
  digitalWrite(C3, HIGH);

  // Faz a leitura da COLUNA 1
  digitalWrite(C1, LOW);
  if (!digitalRead(L1)) b = 1;
  if (!digitalRead(L2)) b = 4;
  if (!digitalRead(L3)) b = 7;
  digitalWrite(C1, HIGH);

  // Faz a leitura da COLUNA 2
  digitalWrite(C2, LOW);
  if (!digitalRead(L1)) b = 2;
  if (!digitalRead(L2)) b = 5;
  if (!digitalRead(L3)) b = 8;
  digitalWrite(C2, HIGH);

  // Faz a leitura da COLUNA 3
  digitalWrite(C3, LOW);
  if (!digitalRead(L1)) b = 3;
  if (!digitalRead(L2)) b = 6;
  if (!digitalRead(L3)) b = 9;

  return b;
}

Esta função faz o aterramento de cada coluna, uma de cada vez, para poder fazer a leitura de cada uma das linhas desta coluna  e determinar qual botão foi pressionado em determinado momento.

Inicialmente ela coloca em nível ALTO todas as portas digitais atribuídas às colunas, fazendo com que estas portas se comportem como se fossem pinos +5V. À medida em que seguimos realizando a leitura de cada coluna nós vamos colocar o pino da coluna como GND (nível BAIXO) e, depois da leitura, vamos colocar o pino novamente como +5V (nível ALTO).

O programa começa fazendo a leitura dos botões da primeira coluna colocando em nível BAIXO a porta digital atribuída a ela, fazendo com que a porta da coluna 1 se comporte como se fosse um pino GND. Desta forma as portas de INPUT irão receber algum sinal somente quando pressionarmos as teclas 1, 4 ou 7.
É realizada então a leitura das portas INPUT para determinar qual destas 3 teclas foi pressionada no teclado. Ao final da leitura a porta atribuída à primeira coluna é colocada novamente em nível ALTO.

Assim que o programa termina de ler os dados da primeira coluna ele repete todo o procedimento anterior, só que agora ele vai colocar em nível BAIXO a porta digital atribuída à coluna 2. Assim as portas de INPUT irão receber algum sinal somente quando pressionarmos as teclas 2, 5 ou 8.
É realizada então a leitura das portas INPUT para determinar qual destas 3 teclas foi pressionada. Ao final da leitura a porta atribuída à segunda coluna é colocada novamente em nível ALTO.

E, finalmente, o programa repete mais uma vez o mesmo processo, só que agora ele vai colocar em nível BAIXO a porta digital atribuída à coluna 3. Desta forma as portas de INPUT irão receber algum sinal somente quando pressionarmos as teclas 3, 6 ou 9.
É realizada então a leitura das portas INPUT para determinar qual destas 3 teclas foi pressionada.


Conclusão

Assim, chegamos ao final do nosso post.

Aprendemos como criar uma versão digital do clássico Jogo da Velha, demonstrando que coisas simples do nosso dia a dia, até mesmo os jogos de lápis, papel e borracha, podem ser sistematizados com o intuito de trazer mais funcionalidades, praticidade, agilidade e economia.

Se você gostou desta versão de Jogo da Velha avalie e deixe o seu comentário!

Até a próxima!


Sobre o Autor


Etienne Gomide

Programador e Desenvolvedor de Sistemas Embarcados. Bacharel em Sistemas de Informação pela Faculdade Federal de Viçosa (FDV). Trabalhou nas empresas CIENTEC e ORIONTEC como programador. Atua como Técnico em TI na Universidade Federal de Viçosa (UFV). Tem como passatempo o desenvolvimento de jogos e projetos para Arduino/esp8266/esp32/stm32.


Eletrogate

19 de setembro de 2023

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!