Desenvolva um jogo da velha imbatível em Java com Minimax!

Olá, entusiastas da programação!

Se você é um entusiasta de programação e jogos, prepare-se para mergulhar no mundo da inteligência artificial aplicada ao desenvolvimento de jogos! Neste post, vamos nos aprofundar na criação de um Jogo da Velha Java Minimax, um projeto que combina a simplicidade do jogo clássico com a complexidade de um algoritmo de busca inteligente. Veremos como o Minimax permite que o computador jogue de forma estratégica, buscando sempre a melhor jogada.

Este tutorial detalhado guiará você desde a configuração inicial até a implementação do algoritmo, e o código será escrito seguindo os princípios SOLID, tornando-o fácil de entender, manter e estender, e, claro, perfeitamente preparado para uma futura interface gráfica com Swing.

Por que Minimax?

Para jogos como o da velha, onde o número de possibilidades é relativamente pequeno, o Minimax é, sem dúvida, uma escolha excelente. Ele explora todas as possibilidades futuras do jogo, simulando jogadas tanto para o jogador humano quanto para o computador. O objetivo é, principalmente, maximizar o ganho do computador (ou minimizar a perda), considerando que o oponente (o humano) também jogará de forma otimizada. Em outras palavras, o Minimax permite que o computador preveja o futuro do jogo e escolha o melhor movimento para alcançar a vitória (ou, na pior das hipóteses, um empate).

Vamos ao código!

Nosso código será organizado em classes, a fim de garantir a modularidade e a aplicação dos princípios SOLID.

Interface Player

Esta interface define o contrato para qualquer jogador no jogo. Assim, garante-se a flexibilidade para adicionar diferentes tipos de jogadores no futuro.

public interface Player {
    int makeMove(Board board);
}

Classe HumanPlayer

Esta classe representa o jogador humano. Aliás, este é o componente que interage diretamente com o usuário.

import java.util.Scanner;

public class HumanPlayer implements Player {
    private final char symbol;
    private final Scanner scanner = new Scanner(System.in);

    public HumanPlayer(char symbol) {
        this.symbol = symbol;
    }

    public char getSymbol() {
        return symbol;
    }

    @Override
    public int makeMove(Board board) {
        int position;
        do {
            System.out.print("Enter your move (1-9): ");
            position = scanner.nextInt() - 1; // Adjust to 0-based index
        } while (!board.isValidMove(position));
        return position;
    }
}

Classe AIPlayer (implementando Minimax)

Esta é, sem dúvida, a parte central do nosso código. Ela implementa a lógica do algoritmo Minimax para o computador. Portanto, é aqui que a mágica acontece.

public class AIPlayer implements Player {
    private final char symbol;
    private final char opponentSymbol;

    public AIPlayer(char symbol, char opponentSymbol) {
        this.symbol = symbol;
        this.opponentSymbol = opponentSymbol;
    }

    public char getSymbol() {
        return symbol;
    }

    @Override
    public int makeMove(Board board) {
        int bestMove = -1;
        int bestScore = Integer.MIN_VALUE;

        for (int i = 0; i < 9; i++) {
            if (board.isValidMove(i)) {
                board.makeMove(i, symbol);
                int score = minimax(board, 0, false); // 'false' significa que é a vez do oponente
                board.undoMove(i); // Desfaz a jogada para explorar outras possibilidades

                if (score > bestScore) {
                    bestScore = score;
                    bestMove = i;
                }
            }
        }
        return bestMove;
    }

    private int minimax(Board board, int depth, boolean isMaximizingPlayer) {
        if (board.isGameOver()) {
            if (board.getWinner() == symbol) {
                return 10 - depth; // Pontuação maior para vitórias mais rápidas
            } else if (board.getWinner() == opponentSymbol) {
                return depth - 10; // Pontuação menor para derrotas mais lentas
            } else {
                return 0; // Empate
            }
        }

        if (isMaximizingPlayer) {
            int bestScore = Integer.MIN_VALUE;
            for (int i = 0; i < 9; i++) {
                if (board.isValidMove(i)) {
                    board.makeMove(i, symbol);
                    int score = minimax(board, depth + 1, false);
                    board.undoMove(i);
                    bestScore = Math.max(bestScore, score);
                }
            }
            return bestScore;
        } else {
            int bestScore = Integer.MAX_VALUE;
            for (int i = 0; i < 9; i++) {
                if (board.isValidMove(i)) {
                    board.makeMove(i, opponentSymbol);
                    int score = minimax(board, depth + 1, true);
                    board.undoMove(i);
                    bestScore = Math.min(bestScore, score);
                }
            }
            return bestScore;
        }
    }
}
Ver mais

Explicação do minimax

  • Caso Base: Se o jogo acabou (alguém venceu ou deu empate), então retorna uma pontuação. Vitória para o computador é positiva, derrota é negativa e empate é zero. A profundidade (depth) é usada, inclusive, para dar preferência a vitórias rápidas e evitar derrotas lentas.
  • Jogador Maximizador (Computador): Tenta maximizar sua pontuação. Para cada movimento possível, simula a jogada, chama recursivamente o minimax para o próximo nível (vez do oponente) e escolhe o movimento que resulta na maior pontuação.
  • Jogador Minimizador (Humano): Tenta minimizar a pontuação do computador. Funciona de maneira análoga ao jogador maximizador, mas, busca a menor pontuação.
  • Recursão: O minimax chama a si mesmo recursivamente, explorando todas as ramificações possíveis do jogo até chegar a um caso base (vitória, derrota ou empate).

Classe Board

Esta classe representa o tabuleiro do jogo e lida, principalmente, com a lógica do jogo.

public class Board {
    private final char[] board;
    private char winner;

    public Board() {
        this.board = new char[9];
        for (int i = 0; i < 9; i++) {
            board[i] = ' ';
        }
        this.winner = ' ';
    }

    public char[] getBoard() {
        return board.clone(); // Retorna uma cópia para evitar modificações externas diretas
    }

    public char getWinner() {
        return winner;
    }

    public void printBoard() {
        System.out.println("-------------");
        for (int i = 0; i < 9; i++) {
            System.out.print("| " + board[i] + " ");
            if ((i + 1) % 3 == 0) {
                System.out.println("|");
                System.out.println("-------------");
            }
        }
    }

    public boolean isValidMove(int position) {
        return position >= 0 && position < 9 && board[position] == ' ';
    }

    public void makeMove(int position, char symbol) {
        board[position] = symbol;
        checkWinner(symbol);
    }

    public void undoMove(int position) {
        board[position] = ' ';
        winner = ' '; // Reseta o vencedor ao desfazer a jogada
    }

    private void checkWinner(char symbol) {
        // Verifica linhas
        for (int i = 0; i < 3; i++) {
            if (board[i * 3] == symbol && board[i * 3 + 1] == symbol && board[i * 3 + 2] == symbol) {
                winner = symbol;
                return;
            }
        }
        // Verifica colunas
        for (int i = 0; i < 3; i++) {
            if (board[i] == symbol && board[i + 3] == symbol && board[i + 6] == symbol) {
                winner = symbol;
                return;
            }
        }
        // Verifica diagonais
        if (board[0] == symbol && board[4] == symbol && board[8] == symbol) {
            winner = symbol;
            return;
        }
        if (board[2] == symbol && board[4] == symbol && board[6] == symbol) {
            winner = symbol;
            return;
        }
    }


    public boolean isGameOver() {
        return winner != ' ' || isBoardFull();
    }

    private boolean isBoardFull() {
        for (int i = 0; i < 9; i++) {
            if (board[i] == ' ') {
                return false;
            }
        }
        return true;
    }
}
Ver mais

Classe TicTacToeGame

Esta classe coordena o jogo. Desse modo, ela une todos os componentes para criar a experiência completa.

public class TicTacToeGame {

    private final Board board;
    private final HumanPlayer humanPlayer;
    private final AIPlayer aiPlayer;
    private Player currentPlayer;

    public TicTacToeGame() {
        this.board = new Board();
        this.humanPlayer = new HumanPlayer('X');
        this.aiPlayer = new AIPlayer('O', humanPlayer.getSymbol());
        this.currentPlayer = humanPlayer;
    }

    public void startGame() {
        System.out.println("Welcome to Tic-Tac-Toe!");
        board.printBoard();

        while (!board.isGameOver()) {
            int move = currentPlayer.makeMove(board);
            board.makeMove(move, ((currentPlayer instanceof HumanPlayer) ? ((HumanPlayer) currentPlayer).getSymbol() : ((AIPlayer) currentPlayer).getSymbol()));
            board.printBoard();

            if (board.getWinner() != ' ') {
                System.out.println("Player " + board.getWinner() + " wins!");
            } else if (board.isGameOver()) {
                System.out.println("It's a draw!");
            }

            currentPlayer = (currentPlayer == humanPlayer) ? aiPlayer : humanPlayer;
        }
    }

    public static void main(String[] args) {
        TicTacToeGame game = new TicTacToeGame();
        game.startGame();
    }
}
Ver mais

Executando o jogo

Compile e execute a classe TicTacToeGame. Você verá, então, o tabuleiro impresso no console. O jogador humano (você) joga com ‘X’ e o computador com ‘O’. Siga as instruções para inserir sua jogada (números de 1 a 9 correspondem às posições no tabuleiro).

TicTacToe game example
Final de uma partida

Preparando para o Swing

A beleza deste código é que ele já está estruturado, de forma que facilita a transição para uma interface gráfica com Swing. Você precisará, portanto:

  1. Substituir a Entrada/Saída: Remova a leitura do input do console na classe HumanPlayer e substitua por componentes Swing (botões, campos de texto, etc.). A ação de clicar em um botão representará, eventualmente, a jogada do jogador humano.
  2. Atualizar a Visualização: Substitua board.printBoard() por uma representação visual do tabuleiro usando componentes Swing. Você pode usar JButton para cada célula do tabuleiro.
  3. Conectar a Lógica: Conecte os eventos de clique dos botões à lógica de makeMove() da classe Board e Player.
  4. Princípios SOLID: As classes seguem os princípios SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion), o que significa que são fáceis de modificar e estender. Por exemplo, você poderia adicionar diferentes níveis de dificuldade ao AI alterando a profundidade da busca Minimax ou introduzindo aleatoriedade.

Conclusão

Parabéns! Você construiu um Jogo da Velha Java Minimax com uma inteligência artificial decente usando o algoritmo Minimax. Este tutorial fornece, antes de mais nada, uma base sólida para explorar conceitos mais avançados de IA em jogos e também demonstra como escrever código limpo, modular e pronto para evoluir. Experimente, explore e divirta-se programando!

Continue na parte 2 para evoluir o código com uma interface gráfica e ajuste de dificuldade.

1 comentário em “Desenvolva um jogo da velha imbatível em Java com Minimax!”

Deixe um comentário