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;
}
}
}
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;
}
}
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();
}
}
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).

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:
- 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.
- 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.
- Conectar a Lógica: Conecte os eventos de clique dos botões à lógica de makeMove() da classe Board e Player.
- 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!”