Projetos

Crie um Jogo da Velha Imbatível com IA no seu Arduino

Abraão da Silva 21 de agosto de 2025

No crescente mundo da Inteligência Artificial, uma das formas de explorar o poder das máquinas é através de algoritmos inteligentes para tomada de decisão. Hoje, vamos embarcar em um projeto prático e divertido: construir um jogo da velha para Arduino com uma estratégia de IA que garante que o computador nunca perca!

O jogo da velha, nosso conhecido “tic-tac-toe”, é um excelente campo de testes para entender como implementar lógica de decisão em sistemas embarcados. Nesta jornada, vamos utilizar uma abordagem específica da IA para criar um oponente virtual no seu Arduino que joga de forma inteligente, sempre buscando a melhor jogada para evitar a derrota e, quando possível, alcançar a vitória. Prepare-se para ter um adversário que, no mínimo, sempre empatará com você!

Neste guia passo a passo, vamos desvendar a estratégia por trás dessa “inteligência” imbatível, mostrando como implementar o algoritmo no seu código Arduino e como criar uma interface simples para jogar através do Monitor Serial. Este projeto é perfeito para quem quer aprender sobre os fundamentos da IA, algoritmos de busca e como aplicar esses conceitos na programação do seu microcontrolador Arduino. Vamos nessa?


Entendendo as Estratégias de Inteligência Artificial para Jogos

No vasto campo da Inteligência Artificial, diferentes abordagens são empregadas para dotar máquinas com a capacidade de “pensar” e tomar decisões. Em jogos, duas categorias principais de IA se destacam: a determinística e a estocástica.

A IA determinística opera sob um conjunto fixo de regras, onde cada ação em um determinado estado sempre levará ao mesmo resultado. Não há espaço para aleatoriedade; a lógica e os cálculos ditam o comportamento. Imagine um robô seguindo um mapa predefinido: para cada instrução (“vire à esquerda”, “avance 5 passos”), a ação e o resultado são sempre os mesmos.

Em contraste, a IA estocástica incorpora elementos de aleatoriedade em seu processo de tomada de decisão. Para a mesma situação, a IA pode escolher ações diferentes com certas probabilidades. Essa abordagem é útil em jogos com informações incompletas (como pôquer, onde as cartas dos oponentes são desconhecidas) ou para introduzir imprevisibilidade e tornar o jogo contra a IA mais parecido com jogar contra um humano. Um exemplo seria um personagem de videogame que, ao avistar o jogador, tem uma probabilidade de 70% de correr para atacá-lo e 30% de se esconder.

Para o nosso projeto do jogo da velha inteligente no Arduino, adotaremos uma abordagem determinística, utilizando o renomado algoritmo Minimax. A escolha por uma IA determinística se deve à natureza perfeitamente conhecida do jogo da velha. Todas as informações estão sempre visíveis para ambos os jogadores, e não há elementos de sorte envolvidos. Nessa situação, uma estratégia baseada em regras fixas e na análise exaustiva de todas as possibilidades pode levar a uma jogabilidade perfeita por parte da IA. O Minimax nos permitirá explorar todas as ramificações do jogo para garantir que o Arduino sempre faça a melhor jogada possível.

O Algoritmo Minimax Explicado

O coração do nosso projeto é o algoritmo minimax – uma técnica de tomada de decisão usada em jogos de soma zero com dois jogadores (como xadrez, damas e jogo da velha). No jogo da velha, esse algoritmo permite que o computador analise todas as jogadas possíveis e suas consequências até o final do jogo, escolhendo sempre o movimento que maximiza suas chances de vitória.

Para entender o minimax, imagine que você está jogando xadrez e planejando vários movimentos à frente: “Se eu mover esta peça, meu oponente provavelmente responderá assim, então eu farei aquilo, e ele fará isso…” – O minimax formaliza exatamente esse processo de pensamento.

Vamos ilustrar com uma analogia:

Imagine que você está em um labirinto com vários caminhos possíveis. Alguns levam a tesouros (+10 pontos), outros a armadilhas (-10 pontos), e outros simplesmente a saídas neutras (0 pontos). O minimax é como se você pudesse ver todos os caminhos possíveis de uma vez e escolher aquele que garante o maior valor, considerando que há um adversário tentando te direcionar para o pior caminho possível.

 

Fonte: Autor

A lógica seguida pelo modelo é parecida com essa estrutura onde a cada jogada o algoritmo analisa as possibilidades e toma a melhor decisão, no caso buscando ganhar +10 (vitória), mas se não for possível busca o 0 (empate). Lembrando que essa imagem é só uma representação do fluxo do processo a quantidade de possibilidades analisadas a cada jogada é bem maior.

No contexto do jogo da velha:

  • O algoritmo cria uma árvore de todos os possíveis estados do jogo a partir do estado atual.
  • Atribui valores aos estados finais: +10 para vitória da IA, -10 para vitória do jogador, 0 para empate.
  • Trabalha de baixo para cima na árvore, alternando entre maximizar (turno da IA) e minimizar (turno do jogador).
  • Escolhe o movimento que leva ao melhor resultado possível, assumindo que o oponente também jogará de forma ótima.

Essa abordagem é possível no jogo da velha porque o espaço de possibilidades é relativamente pequeno. O jogo tem apenas 9 posições, resultando em um máximo de 9! (fatorial de 9) = 362.880 estados possíveis. Na prática, considerando as simetrias e estados impossíveis, o número real é muito menor, cerca de 765 estados finais únicos.


Implementando e entendendo o Algoritmo

Para que o nosso jogo fique ainda mais interessante teremos 3 níveis de dificuldade fácil, médio e difícil e para construir a dificuldade cada nível terá acesso a diferentes níveis da árvore de possibilidades. Ou seja, conforme a dificuldade aumenta a IA consegue ver mais possibilidades a frente, simulando melhor assim o pensamento humano. Assim o nível fácil consegue ver poucos passos a frente, o médio vê alguns passos a mais e o avançado vê todos.

Componentes Necessários

Para este projeto, você precisará de:

Código:

/******************************************************************************************* 
 * Jogo da Velha com Níveis de Dificuldade para Arduino 
 * Interface via Monitor Serial 
 * Este código implementa um jogo da velha com IA usando Minimax com diferentes profundidades
 * Níveis:
 * 1 - Fácil: Minimax com profundidade limitada (pensamento raso)
 * 2 - Médio: Minimax com profundidade intermediária
 * 3 - Difícil: Minimax completo (pensamento profundo)
 *******************************************************************************************/

// Representação do tabuleiro (0 = vazio, 1 = jogador, 2 = Arduino)
int tabuleiro[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};

// Combinações vencedoras
const int VITORIAS[8][3] = {
    {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, // linhas
    {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, // colunas
    {0, 4, 8}, {2, 4, 6}             // diagonais
};

// Símbolos do tabuleiro
char simbolos[3] = {' ', 'X', 'O'};

// Variáveis de controle
bool turnoJogador = true;
bool jogoAtivo = false;
int dificuldade = 1; // 1-Fácil, 2-Médio, 3-Difícil
bool jogadorComeca = true;
bool primeiraPartida = true;  //variável para controlar se é a primeira partida entre todas 

// Pontuação
int jogadorPontos = 0;
int arduinoPontos = 0;
int empates = 0;

void setup() {
    Serial.begin(9600);
    while (!Serial) { ; }
    // Inicializar o gerador de números aleatórios para decidir quem começa a primeira partida
    randomSeed(analogRead(0));
    exibirMenuInicial();
}

void loop() {
    if (Serial.available() > 0) {
        String entrada = Serial.readStringUntil('\n');
        entrada.trim();
        if (entrada.length() > 0) {
            processarEntrada(entrada[0]);
        }
    }
}

void processarEntrada(char entrada) {
    if (!jogoAtivo) {
        switch(entrada) {
            case '1': iniciarNovoJogo(); break;
            case '2': exibirInstrucoes(); break;
            case '3': selecionarDificuldade(); break;
            default: Serial.println("Opção inválida. Digite 1, 2 ou 3.");
        }
    } else if (jogoAtivo && turnoJogador) {
        if (entrada >= '1' && entrada <= '9') {
            int posicao = entrada - '1';
            if (tabuleiro[posicao] == 0) {
                fazerJogada(posicao, 1); // Jogador (X)
                verificarFimDeJogo();
                
                if (jogoAtivo) {
                    //pausa para o jogador ver sua jogada
                    delay(1500);
                    
                    turnoJogador = false;
                    Serial.println("\nArduino está pensando...");
                    delay(500);
                    realizarJogadaAI();
                }
            } else {
                Serial.println("Posição ocupada! Escolha outra (1-9):");
            }
        } else {
            Serial.println("Entrada inválida! Digite um número de 1 a 9:");
        }
    }
}

// Funções de interface
void exibirMenuInicial() {
    Serial.println("\n\n==================================");
    Serial.println("* JOGO DA VELHA INTELIGENTE *");
    Serial.println("==================================");
    Serial.println("1. Iniciar novo jogo");
    Serial.println("2. Instruções");
    Serial.println("3. Selecionar dificuldade");
    Serial.println("==================================");
    Serial.println("Digite uma opção:");
}

void exibirInstrucoes() {
    Serial.println("\n\n==================================");
    Serial.println("* INSTRUÇÕES *");
    Serial.println("==================================");
    Serial.println("- Você será X e o Arduino será O");
    Serial.println("- Digite um número de 1 a 9 para jogar");
    Serial.println("\n  1 | 2 | 3 \n  ---------\n  4 | 5 | 6 \n  ---------\n  7 | 8 | 9 ");
    Serial.println("\nNíveis de dificuldade:");
    Serial.println("1: Fácil (pensamento raso)");
    Serial.println("2: Médio (pensamento moderado)");
    Serial.println("3: Difícil (pensamento profundo)");
    Serial.println("==================================");
}

void selecionarDificuldade() {
    Serial.println("\nSelecione a dificuldade:");
    Serial.println("1. Fácil");
    Serial.println("2. Médio");
    Serial.println("3. Difícil");
    Serial.println("Digite o número (1-3):");

    while (Serial.available() == 0) { delay(100); }
    
    char entrada = Serial.readStringUntil('\n')[0];
    if (entrada >= '1' && entrada <= '3') {
        dificuldade = entrada - '0';
        Serial.print("Dificuldade definida como: ");
        Serial.println(dificuldade);
    } else {
        Serial.println("Opção inválida. Mantendo dificuldade atual.");
    }
    exibirMenuInicial();
}

// Funções do jogo
void iniciarNovoJogo() {
    for (int i = 0; i < 9; i++) tabuleiro[i] = 0;
    jogoAtivo = true;
    
    // Lógica para determinar quem começa
    if (primeiraPartida) {
        // Na primeira partida, escolha aleatória de quem começa
        jogadorComeca = random(2) == 0;  // 50% de chance para cada um
        primeiraPartida = false;  // Não é mais a primeira partida
    } else {
        // Nas partidas seguintes, alterna quem começa
        jogadorComeca = !jogadorComeca;
    }
    
    turnoJogador = jogadorComeca;

    Serial.println("\n\n* NOVO JOGO INICIADO *");
    if (turnoJogador) {
        Serial.println("* Você começa! *");
    } else {
        Serial.println("* Arduino começa! *");
    }
    
    exibirTabuleiro();
    
    if (!turnoJogador) {
        // Adicionamos uma pequena pausa para o jogador ver o tabuleiro inicial
        delay(1000);
        Serial.println("\nArduino está pensando...");
        delay(500);
        realizarJogadaAI();
    } else {
        Serial.println("\nSua vez! Digite um número (1-9):");
    }
}

void exibirTabuleiro() {
    Serial.println("\n  TABULEIRO");
    Serial.println("=============");
    for (int i = 0; i < 9; i += 3) {
        Serial.print(" ");
        for (int j = 0; j < 3; j++) {
            Serial.print(" ");
            if (tabuleiro[i+j] == 0) Serial.print(i+j+1);
            else Serial.print(simbolos[tabuleiro[i+j]]);
            if (j < 2) Serial.print(" |");
        }
        Serial.println();
        if (i < 6) Serial.println(" ---+---+---");
    }
    Serial.println("=============");
}

void fazerJogada(int posicao, int jogador) {
    tabuleiro[posicao] = jogador;
    if (jogador == 1) Serial.println("\nVocê jogou:");
    exibirTabuleiro();
}

void realizarJogadaAI() {

    // Variável para armazenar a posição da melhor jogada encontrada pelo algoritmo
    int melhorJogada = -1;
    // Variável para armazenar a pontuação da melhor jogada
    int melhorPontuacao = -1000;

    // Calcula a melhor jogada baseado na dificuldade
    int profundidadeMaxima;
    switch(dificuldade) {
        case 1: profundidadeMaxima = 1; break; // Fácil: pensamento raso
        case 2: profundidadeMaxima = 4; break; // Médio: pensamento moderado
        case 3: profundidadeMaxima = 8; break; // Difícil: pensamento profundo
        default: profundidadeMaxima = 8;
    }

    // Percorre todas as 9 posições do tabuleiro procurando jogadas possíveis
    for (int i = 0; i < 9; i++) {
        if (tabuleiro[i] == 0) {
            tabuleiro[i] = 2;
            int pontuacao = minimax(tabuleiro, 0, false, profundidadeMaxima);
            tabuleiro[i] = 0;

            // Atualiza a melhor jogada se a pontuação atual for superior
            if (pontuacao > melhorPontuacao) {
                melhorPontuacao = pontuacao;
                melhorJogada = i;
            }
        }
    }

    // Faz a jogada do Arduino
    if (melhorJogada != -1) {
        Serial.println("Arduino jogou:"); 
        fazerJogada(melhorJogada, 2);
        verificarFimDeJogo();
        if (jogoAtivo) {
            turnoJogador = true;
            Serial.println("\nSua vez! Digite um número (1-9):");
        }
    }
}

int minimax(int tabuleiroAtual[], int profundidade, bool maximizando, int profundidadeMaxima) {
    // Verifica se atingiu a profundidade máxima ou fim do jogo
    int resultado = verificarVencedorSimulado(tabuleiroAtual);
    if (resultado != 0 || profundidade >= profundidadeMaxima) {
        if (resultado == 2) return 10 - profundidade;
        else if (resultado == 1) return profundidade - 10;
        else return 0;
    }

    // se maximizando for true busca a jogada que da maior score de pontuação
    if (maximizando) {
        int melhorPontuacao = -1000;
        for (int i = 0; i < 9; i++) {
            if (tabuleiroAtual[i] == 0) {
                tabuleiroAtual[i] = 2;
                int pontuacao = minimax(tabuleiroAtual, profundidade+1, false, profundidadeMaxima);
                tabuleiroAtual[i] = 0;
                melhorPontuacao = max(melhorPontuacao, pontuacao);
            }
        }
        return melhorPontuacao;
    } 
    // simula a vez do jogador para entender o que ele jogará
    else {
        int melhorPontuacao = 1000;
        for (int i = 0; i < 9; i++) {
            if (tabuleiroAtual[i] == 0) {
                tabuleiroAtual[i] = 1;
                int pontuacao = minimax(tabuleiroAtual, profundidade+1, true, profundidadeMaxima);
                tabuleiroAtual[i] = 0;
                melhorPontuacao = min(melhorPontuacao, pontuacao);
            }
        }
        return melhorPontuacao;
    }
}

void verificarFimDeJogo() {
    // chama a funçaõ que valida o estado do tabuleiro final
    int resultado = verificarVencedorSimulado(tabuleiro);

    // dependendo do resultado imprime as saídas possiveis acrescentando as pontuações ao placar
    if (resultado != 0) {
        jogoAtivo = false;
        exibirTabuleiro();
        
        if (resultado == 1) {
            Serial.println("\n*** PARABÉNS! VOCÊ VENCEU! ***");
            jogadorPontos++;
        } else if (resultado == 2) {
            Serial.println("\n*** O ARDUINO VENCEU! ***");
            arduinoPontos++;
        } else {
            Serial.println("\n*** EMPATE! ***");
            empates++;
        }
        // exibe a pontuação e a telça de fim do jogo
        exibirPontuacao();
        exibirFimDeJogo();
    }
}

int verificarVencedorSimulado(int tabuleiro[]) {
    //valida se alguém venceu
    for (int i = 0; i < 8; i++) {
        if (tabuleiro[VITORIAS[i][0]] != 0 &&
            tabuleiro[VITORIAS[i][0]] == tabuleiro[VITORIAS[i][1]] &&
            tabuleiro[VITORIAS[i][0]] == tabuleiro[VITORIAS[i][2]]) {
            return tabuleiro[VITORIAS[i][0]];
        }
    }
    // verifica se o jogo esta em andamento
    for (int i = 0; i < 9; i++) {
        if (tabuleiro[i] == 0) return 0;
    }
    return 3; // Empate
}

void exibirPontuacao() {
    //exibiçaõ da tela de pontos
    Serial.println("\n  PONTUAÇÃO");
    Serial.println("=============");
    Serial.print("Jogador (X): "); Serial.println(jogadorPontos);
    Serial.print("Arduino (O): "); Serial.println(arduinoPontos);
    Serial.print("Empates: "); Serial.println(empates);
    Serial.println("=============");
}

void exibirFimDeJogo() {
    //Exibição da tela de fim de partida
    Serial.println("\n==================================");
    Serial.println("* FIM DE JOGO *");
    Serial.println("==================================");
    Serial.println("1. Jogar novamente");
    Serial.println("2. Instruções");
    Serial.println("3. Alterar dificuldade");
    Serial.println("==================================");
}

Este código implementa um jogo da velha onde você joga contra o Arduino através do Monitor Serial. A principal característica é que o Arduino possui diferentes níveis de “inteligência” (dificuldade) para jogar contra você.

Entendendo o código

1. Variáveis Globais e Constantes:

 

  • tabuleiro[9]: Um array de inteiros que representa o tabuleiro 3×3. Os índices de 0 a 8 correspondem às posições:
    0 | 1 | 2
    --+---+--
    3 | 4 | 5
    --+---+--
    6 | 7 | 8
    

    Os valores nas posições indicam: 0 para vazio, 1 para o jogador humano (‘X’), e 2 para o Arduino (‘O’). É inicializado vazio.

  • VITORIAS[8][3]: Uma constante que armazena as 8 combinações de índices do tabuleiro que resultam em uma vitória (as 3 linhas, as 3 colunas e as 2 diagonais). Usado para verificar o estado do jogo.
  • simbolos[3]: Um array de caracteres para exibir o tabuleiro: um espaço para vazio, ‘X’ para o jogador (1) e ‘O’ para o Arduino (2).
  • turnoJogador: Um booleano que indica de quem é a vez de jogar. true para o jogador, false para o Arduino.
  • jogoAtivo: Um booleano que indica se há um jogo em andamento. true quando um jogo começa, false quando termina (vitória, derrota ou empate) ou quando o menu é exibido.
  • dificuldade: Um inteiro que armazena o nível de dificuldade selecionado (1, 2 ou 3).
  • jogadorComeca: Um booleano usado para alternar quem começa a cada novo jogo.
  • primeiraPartida: Uma variável para definir se é a primeira partida do jogo.
  • jogadorPontos, arduinoPontos, empates: Variáveis para manter o placar das partidas.

2. Função setup():

Esta é a função de inicialização do Arduino, executada uma vez quando o programa inicia. Configura a comunicação serial e imediatamente chama a função para exibir o menu principal do jogo. No setup temos o randomSeed(analogRead(0)) que é usado internamente para definir o tipo de aleatoriedade em  jogadorComeca = random(2) == 0 na função iniciarNovoJogo() assim definimos aleatoriamente quem começa a primeira partida do jogo.

4. Função loop():

Esta é a função principal do Arduino, executada repetidamente. Ela monitora a porta serial. Se algum dado for recebido, lê a linha completa, remove espaços e passa o primeiro caractere lido para a função processarEntrada.

5. Função processarEntrada(char entrada):

Esta função é o coração do tratamento de entrada.

  • Primeiro, verifica se o jogoAtivo é false. Se for, significa que o usuário está no menu inicial ou final, e a entrada (‘1’, ‘2’ ou ‘3’) é interpretada como uma opção de menu.
  • Se jogoAtivo for true E for a vez do turnoJogador, a entrada é esperada ser um número de ‘1’ a ‘9’.
    • Converte o caractere numérico para o índice do array (subtraindo o valor ASCII de ‘1’).
    • Verifica se a posição no tabuleiro está vazia.
    • Se estiver vazia, faz a jogada do jogador (fazerJogada).
    • Verifica se o jogo terminou (verificarFimDeJogo).
    • Se o jogo não terminou, muda para o turno do Arduino, adiciona um pequeno delay e chama realizarJogadaAI.
    • Se a posição estiver ocupada ou a entrada não for um número válido, informa o usuário.

6. Funções de Interface (Display):

  • exibirMenuInicial():

    Imprime o menu principal do jogo no Monitor Serial.

  • exibirInstrucoes():

    Explica as regras básicas do jogo, mostra como as posições correspondem aos números de 1 a 9 e detalha os níveis de dificuldade.

  • selecionarDificuldade():

    Exibe as opções de dificuldade, espera (bloqueando o programa com o while) por uma entrada do usuário. Valida a entrada e atualiza a variável global dificuldade. Em seguida, volta para o menu inicial.

  • exibirTabuleiro():

    Formata e imprime o estado atual do tabuleiro no Monitor Serial, usando os simbolos (‘X’, ‘O’, ou o número da posição vazia).

  • exibirPontuacao():

    Exibe o placar atual do jogo.

  • exibirFimDeJogo():

    Exibe o menu de opções que aparece após o término de uma partida.

7. Funções de Lógica do Jogo:

  • iniciarNovoJogo():

    Prepara o tabuleiro para um novo jogo, reseta a variável jogoAtivo, alterna quem começa a próxima rodada e exibe as informações iniciais. Se for a vez do Arduino, já chama a função para ele jogar.

  • fazerJogada(int posicao, int jogador):

    Esta função executa uma jogada simples: preenche a posição especificada no tabuleiro com o valor do jogador (1 ou 2) e exibe o tabuleiro atualizado.

  • verificarFimDeJogo():

    Verifica se o jogo terminou chamando verificarVencedorSimulado no tabuleiro principal. Se terminou, define jogoAtivo como false, exibe o tabuleiro final, anuncia o resultado, atualiza a pontuação e mostra os menus de pontuação e fim de jogo.

  • verificarVencedorSimulado(int tabuleiro[]):

    Esta função é uma utilidade que verifica o estado de qualquer tabuleiro (passado como argumento, seja o tabuleiro real ou uma cópia usada no Minimax) para determinar se há um vencedor ou um empate. Retorna 1 se o jogador 1 vence, 2 se o jogador 2 vence, 3 se for empate, e 0 se o jogo ainda estiver em andamento.

8. Funções de IA (Minimax):

  • realizarJogadaAI():

    Esta função é responsável por calcular e executar a jogada do Arduino.

    • Determina a profundidadeMaxima para a busca Minimax com base na variável dificuldade.
    • Ele então itera por todas as posições vazias no tabuleiro atual. Para cada posição vazia:
      • Ele simula (faz temporariamente) a jogada do Arduino nessa posição.
      • Chama a função minimax para avaliar a pontuação desse estado do tabuleiro. A chamada inicial para minimax usa false para o parâmetro maximizando, pois depois da jogada do Arduino, o próximo a jogar (na simulação) será o jogador humano, que tenta minimizar o score do Arduino. A profundidade inicial é 0.
      • Ele desfaz a jogada simulada (tabuleiro[i] = 0) para que o tabuleiro volte ao estado original antes de simular a próxima jogada possível (isso é o backtracking essencial para a recursão do Minimax).
      • Compara a pontuação retornada pelo minimax com a melhorPontuacao encontrada até agora e atualiza melhorPontuacao e melhorJogada se a pontuação atual for melhor para o Arduino.
    • Após avaliar todas as posições vazias, ele executa a melhorJogada encontrada chamando fazerJogada.
    • Finalmente, verifica se o jogo terminou após a jogada do Arduino e, se não terminou, passa a vez de volta para o jogador.
  • minimax(int tabuleiroAtual[], int profundidade, bool maximizando, int profundidadeMaxima):

    Esta é a implementação do algoritmo Minimax. É uma função recursiva que explora o “árvore do jogo” (todas as jogadas possíveis a partir de um determinado estado).

    • Parâmetros:
      • tabuleiroAtual[]: Uma cópia do tabuleiro para simular jogadas.
      • profundidade: O nível atual na árvore de busca (começa em 0).
      • maximizando: Um booleano; true significa que é o turno do jogador que tenta maximizar o score (o Arduino), false significa que é o turno do jogador que tenta minimizar o score (o jogador humano).
      • profundidadeMaxima: O limite de profundidade da busca.
    • Condições de Parada (Base Case): A recursão para quando um dos seguintes acontece:
      • Um vencedor ou um empate é encontrado (verificarVencedorSimulado retorna algo diferente de 0).
      • A profundidade máxima de busca (profundidadeMaxima) definida pela dificuldade é atingida.
    • Pontuação das Bases:
      • Se o Arduino (2) venceu: Retorna 10 - profundidade. Quanto menor a profundidade (mais rápido a vitória), maior o score.
      • Se o Jogador (1) venceu: Retorna profundidade - 10. Quanto menor a profundidade (mais rápido a derrota), menor (mais negativo) o score. Isso faz com que o Arduino evite estados onde o jogador ganha rapidamente dentro do limite de busca.
      • Se for empate ou a profundidade máxima foi atingida sem um resultado final na simulação: Retorna 0.
    • Passo Recursivo:
      • Se maximizando é true (vez do Arduino na simulação): O objetivo é encontrar a jogada que leva ao maior score possível. Ele itera sobre as posições vazias, simula a jogada do Arduino, chama minimax recursivamente para o próximo nível (profundidade + 1) onde o jogador humano estará minimizando (false), e escolhe o máximo score retornado.
      • Se maximizando é false (vez do Jogador na simulação): O objetivo é encontrar a jogada que leva ao menor score possível (do ponto de vista do Arduino). Ele itera sobre as posições vazias, simula a jogada do jogador, chama minimax recursivamente para o próximo nível (profundidade + 1) onde o Arduino estará maximizando (true), e escolhe o mínimo score retornado.
    • As jogadas simuladas são desfeitas (tabuleiroAtual[i] = 0) após a chamada recursiva para não afetar as outras ramificações da árvore de busca (backtracking).

Em resumo, o código funciona da seguinte forma:

  1. Inicializa a comunicação serial e exibe o menu principal.
  2. O loop principal fica esperando por entradas no Monitor Serial.
  3. A função processarEntrada direciona a entrada para as funções apropriadas, dependendo se um jogo está ativo ou se o usuário está interagindo com o menu.
  4. No menu, o usuário pode iniciar um novo jogo, ver instruções ou mudar a dificuldade.
  5. Quando um novo jogo começa, o tabuleiro é resetado, os turnos são definidos e o tabuleiro é exibido. Se o Arduino começa, ele joga imediatamente.
  6. Durante o jogo, se for a vez do jogador, a entrada esperada é um número de 1 a 9. O código valida a entrada e a posição. Se válida, faz a jogada do jogador, verifica o fim do jogo e, se o jogo continuar, passa a vez para o Arduino.
  7. Quando é a vez do Arduino (realizarJogadaAI), ele usa o algoritmo Minimax para decidir a melhor jogada.
  8. O Minimax (minimax) explora as possíveis sequências de jogadas até uma certa profundidade (definida pela dificuldade) ou até que um vencedor/empate seja alcançado na simulação. Ele atribui pontuações aos estados finais e usa a recursão para “propagá-las” de volta, assumindo que ambos os jogadores jogam otimamente (o Arduino maximiza seu score, o jogador minimiza o score do Arduino).
  9. realizarJogadaAI escolhe a jogada real que leva ao estado inicial simulado com a melhor pontuação Minimax encontrada.
  10. Após a jogada do Arduino, o jogo verifica novamente se terminou.
  11. Se o jogo terminar, o resultado é anunciado, a pontuação é atualizada e o menu de fim de jogo é exibido, permitindo ao usuário jogar novamente ou retornar ao menu principal.

 


Conclusão

Parabéns! Você acaba de implementar um jogo da velha com uma IA determinística que joga perfeitamente utilizando o algoritmo minimax. Este projeto ilustra conceitos fundamentais de algoritmos de busca, teoria dos jogos e tomada de decisão – blocos de construção essenciais para compreender sistemas de IA mais complexos.

O conhecimento adquirido pode ser estendido para jogos mais complexos e outros domínios de tomada de decisão. Aqui estão algumas ideias para expandir este projeto:

  1. Interface física: Adicione LEDs e botões para criar uma versão física do jogo, eliminando a necessidade do Monitor Serial.
  2. Níveis de dificuldade: Modifique o algoritmo para permitir que a IA cometa erros ocasionais, criando diferentes níveis de dificuldade.
  3. Jogos mais complexos: Aplique o conceito do minimax para implementar outros jogos como Damas ou Connect Four.
  4. Otimizações: Implemente a poda alfa-beta para melhorar a eficiência do algoritmo, permitindo análises mais profundas em jogos complexos.
  5. Visualização da árvore de decisão: Crie uma versão para PC que mostre visualmente a árvore de decisão sendo explorada pelo algoritmo.

Além de IAs deterministicas temos outras soluções, mas é bom se atentar aos limites computacionais do arduino, ou sistema que esteja desenvolvendo, alguns modelos podem requerer bastante processamento e treinamento.

Referências

  1. Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach (4th ed.). Pearson.
  2. Millington, I., & Funge, J. (2019). Artificial Intelligence for Games (3rd ed.). CRC Press.
  3. Coppin, B. (2015). Artificial Intelligence Illuminated. Jones & Bartlett Learning.
  4. Knuth, D. E., & Moore, R. W. (1975). An analysis of alpha-beta pruning. Artificial Intelligence, 6(4), 293-326.
  5. Neumann, J. V., & Morgenstern, O. (1944). Theory of Games and Economic Behavior. Princeton University Press.

 


Abraão da Silva

21 de agosto de 2025

Estudante de Engenharia da Computação, especializado em curiosidades aparentemente aleatórias e desenvolvimento de software. Se eu não estiver pedalando agora estou estudando ou tentando aproveitar a energia dos raios.

Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!

Eletrogate Robô

Assine nossa newsletter e
receba  10% OFF  na sua
primeira compra!