Desvende o mundo dos labirintos: Crie o seu com Java e Swing!

Olá, exploradores do código! Hoje vamos embarcar em uma jornada fascinante para construir um gerador de labirintos em Java com a ajuda do Swing. Além disso, vamos criar uma base sólida, seguindo os princípios do SOLID e pensando na implementação futura do algoritmo A* para encontrar caminhos. Preparem seus teclados, pois a aventura está apenas começando!

Por que criar um gerador de labirintos?

Criar um gerador de labirintos é um ótimo exercício para aprimorar suas habilidades de programação. É um projeto que envolve lógica, estruturas de dados e a criação de uma interface gráfica intuitiva. Além disso, um projeto como este pode ser a base para projetos de jogos mais complexos, onde o jogador precisa desvendar desafios e encontrar o caminho certo para atingir seus objetivos.

A seguir, exploraremos o código detalhado, que é construído em cima de princípios de design de software que você pode reaproveitar nos seus próprios projetos.

Próximo passo: Padrões SOLID e arquitetura do projeto

Antes de tudo, precisamos garantir que nosso código seja limpo, organizado e fácil de manter. Para isso, vamos aplicar os princípios SOLID:

  • S (Single Responsibility Principle): Cada classe terá uma única responsabilidade bem definida.
  • O (Open/Closed Principle): Nossas classes estarão abertas para extensão, mas fechadas para modificação.
  • L (Liskov Substitution Principle): Subtipos poderão substituir seus tipos base sem quebrar o programa.
  • I (Interface Segregation Principle): Usaremos interfaces específicas para cada cliente.
  • D (Dependency Inversion Principle): Dependeremos de abstrações (interfaces) em vez de classes concretas.

Nesse sentido, dividiremos o projeto em várias classes:

  1. Cell: Representa cada célula do labirinto.
  2. MazeGenerator: Interface para gerar o labirinto.
  3. RecursiveBacktrackerGenerator: Uma implementação concreta do gerador, usando o algoritmo de retrocesso recursivo.
  4. MazePanel: Painel Swing para desenhar o labirinto na tela.
  5. Main: A classe principal que orquestra tudo.

Vamos ao código: A magia em Java!

1. A classe Cell.java

import java.awt.*;

public class Cell {
    public int row, col;
    public boolean wallNorth, wallEast, wallSouth, wallWest;
    public boolean visited = false;
    public Cell previousCell;

    public Cell(int row, int col) {
        this.row = row;
        this.col = col;
        this.wallNorth = true;
        this.wallEast = true;
        this.wallSouth = true;
        this.wallWest = true;
        this.previousCell = null;
    }

    public Point getPoint(){
        return new Point(this.row,this.col);
    }
}
Ver mais

A classe Cell é um componente fundamental do nosso projeto. Nela, armazenamos informações importantes como:

  • row e col: As coordenadas da célula no labirinto.
  • wallNorth, wallEast, wallSouth, wallWest: Variáveis booleanas que indicam se a célula tem paredes em cada direção.
  • visited: Indica se a célula já foi visitada pelo algoritmo de geração.
  • previousCell: Mantém o rastro do caminho usado durante a geração.

2. A interface MazeGenerator.java

public interface MazeGenerator {
    Cell[][] generateMaze(int rows, int cols);
}

Como resultado, definimos uma interface simples que garante que todos os geradores de labirinto sigam um padrão. Ela tem um método generateMaze, que recebe a quantidade de linhas e colunas do labirinto e retorna uma matriz de Cell.

3. A classe RecursiveBacktrackerGenerator.java

import java.util.Random;
import java.util.Stack;

public class RecursiveBacktrackerGenerator implements MazeGenerator {

    @Override
    public Cell[][] generateMaze(int rows, int cols) {
        Cell[][] grid = createGrid(rows, cols);
        recursiveBacktracker(grid, 0, 0);
        return grid;
    }

    private Cell[][] createGrid(int rows, int cols) {
        Cell[][] grid = new Cell[rows][cols];
        for (int row = 0; row < rows; row++) {
            for (int col = 0; col < cols; col++) {
                grid[row][col] = new Cell(row, col);
            }
        }

        return grid;
    }

    private void recursiveBacktracker(Cell[][] grid, int startRow, int startCol) {
        Stack<Cell> stack = new Stack<>();
        Cell current = grid[startRow][startCol];
        current.visited = true;
        stack.push(current);

        while (!stack.isEmpty()) {
            current = stack.peek();
            Cell neighbor = getUnvisitedNeighbor(grid, current);

            if (neighbor != null) {
                removeWall(current, neighbor);
                neighbor.visited = true;
                neighbor.previousCell = current;
                stack.push(neighbor);
            } else {
                stack.pop();
            }
        }
    }


    private Cell getUnvisitedNeighbor(Cell[][] grid, Cell cell) {
        int row = cell.row;
        int col = cell.col;
        int[][] neighborsPosition = {{row - 1,col},{row, col+1},{row+1,col},{row,col-1}};

        java.util.List<Cell> neighbors = new java.util.ArrayList<Cell>();

        for (int[] pos: neighborsPosition){
          int newRow = pos[0];
            int newCol = pos[1];
            if (isValid(grid, newRow,newCol)){
                Cell n = grid[newRow][newCol];
                if (!n.visited) {
                    neighbors.add(n);
                }
            }
        }
        if (neighbors.size() == 0) return null;
        Random random = new Random();
        return  neighbors.get(random.nextInt(neighbors.size()));
    }

    private void removeWall(Cell current, Cell neighbor) {
        int row = current.row - neighbor.row;
        int col = current.col - neighbor.col;

        if (row == 1) {
            current.wallNorth = false;
            neighbor.wallSouth = false;
        } else if (row == -1) {
            current.wallSouth = false;
            neighbor.wallNorth = false;
        }
        if (col == 1) {
            current.wallWest = false;
            neighbor.wallEast = false;
        } else if (col == -1) {
            current.wallEast = false;
            neighbor.wallWest = false;
        }
    }

    private boolean isValid(Cell[][] grid, int row, int col) {
        return row >= 0 && row < grid.length && col >= 0 && col < grid[0].length;
    }

}
Ver mais

Essa classe implementa o algoritmo de retrocesso recursivo. Primeiramente, ela cria um grid de células e inicia a geração do labirinto com recursiveBacktracker. Esse algoritmo escolhe células vizinhas aleatoriamente e marca-as como visitadas até ter percorrido todo o grid, removendo paredes ao longo do caminho.

4. A classe MazePanel.java

import javax.swing.*;
import java.awt.*;

public class MazePanel extends JPanel {
    private Cell[][] maze;
    private int cellSize = 20;
    private final int borderWidth = 2;
    private int initialCellRow;
    private int initialCellCol;
    private int finalCellRow;
    private int finalCellCol;
    public MazePanel(Cell[][] maze) {
        this.maze = maze;
        setPreferredSize(new Dimension(maze[0].length * cellSize + 2 * borderWidth, maze.length * cellSize + 2 * borderWidth));

        this.initialCellRow = 0;
        this.initialCellCol = 0;

        this.finalCellRow = maze.length -1;
        this.finalCellCol = maze[0].length -1;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        g.setColor(Color.WHITE);
        g.fillRect(0,0,getWidth(), getHeight());
        for (int row = 0; row < maze.length; row++) {
            for (int col = 0; col < maze[0].length; col++) {
                drawCell(g, maze[row][col],  col, row);
            }
        }

        drawStartingAndFinalPositions(g);
    }

    private void drawStartingAndFinalPositions(Graphics g){
        g.setColor(Color.GREEN);
        int startX = borderWidth + initialCellCol * cellSize;
        int startY = borderWidth + initialCellRow * cellSize;
        g.fillRect(startX,startY, cellSize, cellSize );

        g.setColor(Color.RED);
        int endX =  borderWidth + finalCellCol* cellSize;
        int endY = borderWidth + finalCellRow * cellSize;
        g.fillRect(endX,endY,cellSize, cellSize);
    }

    private void drawCell(Graphics g, Cell cell, int x, int y) {
        int cellX = borderWidth + x * cellSize;
        int cellY = borderWidth + y * cellSize;
        g.setColor(Color.BLACK);

        if (cell.wallNorth) {
            g.drawLine(cellX, cellY, cellX + cellSize, cellY);
        }

        if (cell.wallEast) {
            g.drawLine(cellX + cellSize, cellY, cellX + cellSize, cellY + cellSize);
        }

        if (cell.wallSouth) {
            g.drawLine(cellX, cellY + cellSize, cellX + cellSize, cellY + cellSize);
        }

        if (cell.wallWest) {
            g.drawLine(cellX, cellY, cellX, cellY + cellSize);
        }
    }

    public int getInitialCellCol() {
        return initialCellCol;
    }

    public int getInitialCellRow() {
        return initialCellRow;
    }


    public int getFinalCellCol() {
        return finalCellCol;
    }

    public int getFinalCellRow() {
        return finalCellRow;
    }
}
Ver mais

A classe MazePanel é responsável por exibir o labirinto. Nesse caso, o paintComponent itera sobre todas as células do labirinto e desenha as paredes, o início com um retângulo verde, e o final com um retângulo vermelho. Por exemplo, a dimensão do labirinto será definida pela quantidade de células informada, juntamente com a constante cellSize que define a largura de cada célula.

5. A classe Main.java

import javax.swing.*;

public class Main {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            String rowsStr = JOptionPane.showInputDialog("Digite o número de linhas do labirinto:");
            String colsStr = JOptionPane.showInputDialog("Digite o número de colunas do labirinto:");

            int rows;
            int cols;

            try {
                rows = Integer.parseInt(rowsStr);
                cols = Integer.parseInt(colsStr);
                if(rows <= 0 || cols <= 0){
                    JOptionPane.showMessageDialog(null, "Por favor, insira valores positivos maiores que 0");
                    return;
                }
            } catch (NumberFormatException e) {
                JOptionPane.showMessageDialog(null, "Entrada inválida. Use apenas números inteiros.");
                return;
            }

            MazeGenerator generator = new RecursiveBacktrackerGenerator();
            Cell[][] maze = generator.generateMaze(rows, cols);

            JFrame frame = new JFrame("Gerador de Labirintos");
            MazePanel mazePanel = new MazePanel(maze);
            frame.add(mazePanel);

            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.pack();
            frame.setLocationRelativeTo(null); // Centraliza a janela
            frame.setVisible(true);
        });
    }
}
Ver mais

Finalmente, na classe Main, configuramos o programa para iniciar a criação do labirinto. Aqui, criamos uma instância do gerador, criamos o JFrame, o MazePanel e adicionamos ao frame, juntamente com as configurações para centralizar a janela.

Código em ação e considerações finais

Agora você pode executar o código. Verá um labirinto sendo gerado com paredes pretas, uma borda em volta dele, ponto inicial em verde no canto superior esquerdo, e o ponto final no canto inferior direito em vermelho.

Gerador de labirintos em Java com Swing

O projeto está estruturado, utilizando princípios SOLID, tornando-o mais fácil de manter, testar e expandir. Além disso, está pronto para a futura implementação do algoritmo A*. Isso pode ser feito através da criação de uma classe que utiliza os métodos da classe Cell para calcular o caminho ideal de uma célula inicial para a final. Ou seja, você tem a base sólida para continuar aprimorando seu gerador de labirintos e explorar o fascinante mundo dos algoritmos de busca!

Dessa forma, se você chegou até aqui, meus parabéns! Você tem um gerador de labirintos em Java funcional, que pode servir como ponto de partida para outros projetos. Deixe seu comentário abaixo, pois, adoraria saber suas opiniões!

Espero que este guia seja útil e inspirador! Sinta-se à vontade para explorar e customizar o projeto. Se tiver dúvidas ou quiser compartilhar suas criações, a seção de comentários está à disposição. Bons estudos!

Veja também o tutorial de desenvolvimento de um jogo da velha com IA (minimax).

1 comentário em “Desvende o mundo dos labirintos: Crie o seu com Java e Swing!”

Deixe um comentário