Jogo da velha com Swing e Minimax: Uma experiência interativa em Java!

Olá, entusiastas da programação!

Olá, entusiastas da programação! Se você acompanhou nosso último artigo, onde construímos um Jogo da Velha Java Minimax na linha de comando, prepare-se para levar sua experiência a um novo nível. Hoje, vamos transformar aquele código em algo visual e interativo, adicionando uma interface gráfica usando o Swing. Dessa forma, estaremos construindo um completo Jogo da Velha Java Swing Minimax, além de ajustar a dificuldade da IA. Assim, você poderá jogar com o mouse e, inclusive, desafiar o computador em diferentes níveis de dificuldade.

A transição para o gráfico: Swing em ação!

Primeiramente, vamos nos concentrar na criação da interface gráfica. Afinal, ela é a porta de entrada para a interação com nosso jogo. Utilizaremos componentes Swing, como JFrame, JPanel, e JButton, para construir um tabuleiro de jogo amigável e responsivo. Além disso, o jogador poderá interagir diretamente com o tabuleiro usando o mouse, o que torna a experiência mais natural e intuitiva.

Implementando a interface gráfica com Swing

Dessa forma, vamos começar com a criação da classe principal que extenderá JFrame e conterá nosso tabuleiro.

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class TicTacToeSwing extends JFrame {

    private final Board board;
    private final AIPlayer aiPlayer;
    private final HumanPlayer humanPlayer;
    private Player currentPlayer;
    private JButton[][] buttons;
    private JComboBox<String> difficultyComboBox;
    private final int EASY = 1;
    private final int MEDIUM = 4;
    private final int HARD = 7;


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

        setupUI();
        setupDifficultySelection();
    }


    private void setupUI() {
        setTitle("Jogo da Velha");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout());
        setSize(400, 450);
        setLocationRelativeTo(null); // Center the window

        JPanel gamePanel = new JPanel(new GridLayout(3, 3));
        buttons = new JButton[3][3];

        // Set up each button
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                JButton button = new JButton("");
                button.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 80));
                button.addActionListener(new ButtonClickListener(i * 3 + j));
                buttons[i][j] = button;
                gamePanel.add(button);
            }
        }
        add(gamePanel, BorderLayout.CENTER);
        setVisible(true);
    }

    private void setupDifficultySelection(){
        JPanel panel = new JPanel(new FlowLayout());

        JLabel difficultyLabel = new JLabel("Nível de dificuldade: ");

        String[] difficulties = {"Fácil", "Médio", "Difícil"};
        difficultyComboBox = new JComboBox<>(difficulties);
        difficultyComboBox.addActionListener(new DifficultyListener());
        panel.add(difficultyLabel);
        panel.add(difficultyComboBox);

        add(panel, BorderLayout.NORTH);
    }

    private void updateBoard(){
        for (int i = 0; i < 3; i++) {
            for(int j = 0; j<3; j++){
                char symbol = board.getBoard()[i * 3 + j];
                buttons[i][j].setText(String.valueOf(symbol));
            }
        }
        if (board.isGameOver()) {
            if (board.getWinner() != ' ') {
                JOptionPane.showMessageDialog(this, "Jogador " + board.getWinner() + " venceu!");
            } else {
                JOptionPane.showMessageDialog(this, "Deu empate!");
            }
            disableAllButtons();
            resetBoard();
        } else if (currentPlayer instanceof AIPlayer){
            aiMove();
        }

    }

    private void aiMove(){
        int move = aiPlayer.makeMove(board);
        board.makeMove(move, ((AIPlayer) currentPlayer).getSymbol());
        currentPlayer = humanPlayer;
        updateBoard();
    }

    private void resetBoard(){
        for (int i = 0; i < 3; i++) {
            for(int j = 0; j<3; j++){
                buttons[i][j].setText("");
                buttons[i][j].setEnabled(true);
            }
        }
        board.reset();
        currentPlayer = humanPlayer;
    }

    private void disableAllButtons(){
        for (int i = 0; i < 3; i++) {
            for(int j = 0; j<3; j++){
                buttons[i][j].setEnabled(false);
            }
        }
    }

    private class ButtonClickListener implements ActionListener {
        private final int position;

        public ButtonClickListener(int position) {
            this.position = position;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (currentPlayer instanceof HumanPlayer && board.isValidMove(position)){
                board.makeMove(position, ((HumanPlayer) currentPlayer).getSymbol());
                currentPlayer = aiPlayer;
                updateBoard();
            }
        }
    }


    private class DifficultyListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {

            JComboBox<String> combo = (JComboBox<String>)e.getSource();

            String selectedOption = (String)combo.getSelectedItem();

            switch (selectedOption){
                case "Fácil":
                    aiPlayer.setMaxDepth(EASY);
                    break;
                case "Médio":
                    aiPlayer.setMaxDepth(MEDIUM);
                    break;
                case "Difícil":
                    aiPlayer.setMaxDepth(HARD);
                    break;
            }
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(TicTacToeSwing::new);
    }
}
Ver mais

Alterações na classe AIPlayer para suportar níveis de dificuldade

Agora, para suportar níveis de dificuldade, adicionamos uma variável maxDepth que controla a profundidade de busca do Minimax. Assim, podemos limitar a quantidade de jogadas que a IA prevê no futuro.

A classe Board é refatorada para suportar o reset do jogo

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

    public Board() {
        this.board = new char[9];
        reset();
        this.winner = ' ';
    }

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

    public char[] getBoard() {
        return board.clone();
    }

    public char getWinner() {
        return winner;
    }

    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 = ' ';
    }

    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

Executando o jogo

Compile e execute a classe TicTacToeSwing. Uma janela com o tabuleiro do jogo será exibida. Agora, clique nas casas vazias do tabuleiro para fazer suas jogadas e desafiar a IA com diferentes níveis de dificuldade.

Jogo da velha em Java com Swing

Explorando o código e ajustando a Dificuldade

Primeiramente, observe como a classe TicTacToeSwing organiza os componentes do jogo: os botões, o painel, a seleção de dificuldade. A interface foi construída para ser interativa, permitindo que o usuário interaja de forma intuitiva com o jogo. Ademais, você pode perceber como é fácil alterar a profundidade máxima do algoritmo Minimax usando o JComboBox, o que, por conseguinte, torna possível controlar a dificuldade da IA. A classe Board, além disso, foi refatorada para suportar o reset do jogo, tornando o ciclo de gameplay contínuo.

Conclusão

Parabéns! Você transformou seu Jogo da Velha Java Minimax de linha de comando em uma experiência gráfica e interativa usando Swing. Agora, o código está mais robusto, funcional e, principalmente, permite ajustar a dificuldade da IA.

Este projeto ilustra bem como usar os conceitos de orientação a objetos (como os princípios SOLID) e a recursão do Minimax para criar uma aplicação de jogos que, decerto, pode ser aprimorada e expandida para projetos maiores e mais complexos.

Experimente, explore e divirta-se com seus novos conhecimentos! Em resumo, este é um passo importante no caminho da criação de jogos mais complexos e desafiadores.

Deixe um comentário