Trabalho Final - Estrutura de Dados II
UNESP - Universidade Estadual Paulista "Júlio de Mesquita Filho"
Bacharelado em Sistemas de Informação
Este repositório apresenta o desenvolvimento de um jogo no estilo clássico Pac-Man, utilizando a linguagem Java e conceitos de Orientação a Objetos. O diferencial deste projeto está na aplicação prática de 4 Algoritmos de Busca em Grafos para controlar a inteligência artificial dos fantasmas, cada um com comportamento único e estratégias distintas.
- Aplicar conceitos de Teoria dos Grafos em um problema real
- Implementar e comparar algoritmos clássicos de busca
- Desenvolver inteligência artificial para NPCs (fantasmas)
- Analisar complexidade computacional em tempo real
- Utilizar Design Patterns (Facade, Template Method)
- Java JDK 11 ou superior
- Biblioteca Gson (incluída em
lib/gson-2.8.9.jar)
compile.bat
run.batNão é necessário compilação manual - o projeto já está pronto para execução. Se necessário, use os comandos Java padrão.
O projeto utiliza 4 algoritmos clássicos de busca em grafos, cada um controlando um fantasma diferente:
Algoritmo de Dijkstra garante encontrar o caminho mais curto entre dois pontos em um grafo com pesos não-negativos.
- Complexidade: O((V + E) log V)
- Estrutura: Priority Queue (fila de prioridade)
- Garantia: Sempre encontra o caminho ótimo
public static List<Node> findPath(Node start, Node goal) {
// Mapa de distâncias (infinito por padrão)
Map<Node, Double> distances = new HashMap<>();
distances.put(start, 0.0);
// Mapa de predecessores para reconstruir o caminho
Map<Node, Node> predecessors = new HashMap<>();
// Fila de prioridade ordenada por distância
PriorityQueue<NodeDistance> pq = new PriorityQueue<>(
Comparator.comparingDouble(nd -> nd.distance)
);
pq.offer(new NodeDistance(start, 0.0));
Set<Node> visited = new HashSet<>();
while (!pq.isEmpty()) {
NodeDistance current = pq.poll();
Node currentNode = current.node;
if (visited.contains(currentNode)) continue;
visited.add(currentNode);
// Se chegamos ao objetivo, reconstruir e retornar o caminho
if (currentNode.equals(goal)) {
return reconstructPath(predecessors, start, goal);
}
// Explorar todos os vizinhos
for (Node neighbor : currentNode.getAllNeighbors()) {
if (visited.contains(neighbor)) continue;
// Peso uniforme = 1 para todas as arestas
double newDistance = distances.get(currentNode) + 1.0;
double currentDistance = distances.getOrDefault(neighbor, Double.POSITIVE_INFINITY);
// Se encontramos um caminho melhor, atualizar
if (newDistance < currentDistance) {
distances.put(neighbor, newDistance);
predecessors.put(neighbor, currentNode);
pq.offer(new NodeDistance(neighbor, newDistance));
}
}
}
return new ArrayList<>(); // Nenhum caminho encontrado
}@Override
protected Direction chooseDirection(int pacmanX, int pacmanY, Direction pacmanDirection) {
// Perseguição direta usando Dijkstra - sempre o caminho mais curto
Direction nextDir = pathfindingManager.getNextDirectionDijkstra(
this.x, this.y, pacmanX, pacmanY
);
// Se Dijkstra não retornou uma direção válida, manter a direção atual
if (nextDir == Direction.NONE) {
return this.direction;
}
return nextDir;
}🎯 Comportamento: Blinky é o fantasma mais perigoso! Ele sempre persegue o Pac-Man pelo caminho mais curto possível, sendo agressivo, direto e implacável.
Algoritmo A* combina o custo real do caminho com uma heurística (distância de Manhattan) para encontrar caminhos de forma eficiente.
- Complexidade: O(E log V) em casos médios
- Heurística: Distância de Manhattan
- Função: f(n) = g(n) + h(n)
- g(n) = custo real do início até n
- h(n) = estimativa heurística até o objetivo
public static List<Node> findPath(Node start, Node goal) {
// gScore: custo real do início até cada nó
Map<Node, Double> gScore = new HashMap<>();
gScore.put(start, 0.0);
// fScore: gScore + heurística
Map<Node, Double> fScore = new HashMap<>();
fScore.put(start, heuristic(start, goal));
// Predecessores para reconstruir o caminho
Map<Node, Node> predecessors = new HashMap<>();
// Open set: nós a serem avaliados
PriorityQueue<NodeScore> openSet = new PriorityQueue<>(
Comparator.comparingDouble(ns -> ns.fScore)
);
openSet.offer(new NodeScore(start, fScore.get(start)));
Set<Node> closedSet = new HashSet<>();
while (!openSet.isEmpty()) {
NodeScore current = openSet.poll();
Node currentNode = current.node;
// Se chegamos ao objetivo, reconstruir caminho
if (currentNode.equals(goal)) {
return reconstructPath(predecessors, start, goal);
}
closedSet.add(currentNode);
// Explorar vizinhos
for (Node neighbor : currentNode.getAllNeighbors()) {
if (closedSet.contains(neighbor)) continue;
// Calcular novo gScore
double tentativeGScore = gScore.get(currentNode) + 1.0;
double currentGScore = gScore.getOrDefault(neighbor, Double.POSITIVE_INFINITY);
// Se encontramos um caminho melhor
if (tentativeGScore < currentGScore) {
predecessors.put(neighbor, currentNode);
gScore.put(neighbor, tentativeGScore);
double newFScore = tentativeGScore + heuristic(neighbor, goal);
fScore.put(neighbor, newFScore);
openSet.offer(new NodeScore(neighbor, newFScore));
}
}
}
return new ArrayList<>();
}
// Heurística de Manhattan
private static double heuristic(Node from, Node to) {
Position fromPos = from.getPosition();
Position toPos = to.getPosition();
return fromPos.manhattanDistance(toPos);
}@Override
protected Direction chooseDirection(int pacmanX, int pacmanY, Direction pacmanDirection) {
// Previsão: calcular 4 tiles à frente da direção do Pacman
int targetX = pacmanX + (pacmanDirection.dx * tileSize * 4);
int targetY = pacmanY + (pacmanDirection.dy * tileSize * 4);
// Usar A* para emboscada eficiente
Direction nextDir = pathfindingManager.getNextDirectionAStar(
this.x, this.y, targetX, targetY
);
// Se A* não retornou uma direção válida, tentar ir direto ao Pacman
if (nextDir == Direction.NONE) {
nextDir = pathfindingManager.getNextDirectionAStar(
this.x, this.y, pacmanX, pacmanY
);
}
if (nextDir == Direction.NONE) {
return this.direction;
}
return nextDir;
}🎯 Comportamento: Pinky é estratégico e astuto! Ele não persegue diretamente, mas tenta emboscar o Pac-Man prevendo sua posição futura (4 tiles à frente), usando A* para chegar lá rapidamente.
BFS (Breadth-First Search) explora o grafo em "ondas" concêntricas, garantindo encontrar o caminho com menor número de arestas.
- Complexidade: O(V + E)
- Estrutura: Queue FIFO (fila)
- Exploração: Camada por camada
public static List<Node> findPath(Node start, Node goal) {
// Fila FIFO para BFS
Queue<Node> queue = new LinkedList<>();
queue.offer(start);
// Conjunto de nós visitados
Set<Node> visited = new HashSet<>();
visited.add(start);
// Mapa de predecessores para reconstruir o caminho
Map<Node, Node> predecessors = new HashMap<>();
while (!queue.isEmpty()) {
Node current = queue.poll();
// Se chegamos ao objetivo, reconstruir caminho
if (current.equals(goal)) {
return reconstructPath(predecessors, start, goal);
}
// Explorar todos os vizinhos
for (Node neighbor : current.getAllNeighbors()) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
predecessors.put(neighbor, current);
queue.offer(neighbor);
}
}
}
return new ArrayList<>();
}
// Método auxiliar para calcular distância
public static int getDistance(Node start, Node goal) {
Queue<Node> queue = new LinkedList<>();
queue.offer(start);
Set<Node> visited = new HashSet<>();
visited.add(start);
Map<Node, Integer> distances = new HashMap<>();
distances.put(start, 0);
while (!queue.isEmpty()) {
Node current = queue.poll();
int currentDist = distances.get(current);
if (current.equals(goal)) {
return currentDist;
}
for (Node neighbor : current.getAllNeighbors()) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
distances.put(neighbor, currentDist + 1);
queue.offer(neighbor);
}
}
}
return -1; // Nenhum caminho encontrado
}@Override
protected Direction chooseDirection(int pacmanX, int pacmanY, Direction pacmanDirection) {
// Calcular distância até o Pacman usando BFS
int distance = pathfindingManager.getDistanceBFS(this.x, this.y, pacmanX, pacmanY);
Direction nextDir;
if (distance > FLEE_DISTANCE || distance == -1) {
// Longe: perseguir o Pacman
nextDir = pathfindingManager.getNextDirectionBFS(
this.x, this.y, pacmanX, pacmanY
);
} else {
// Perto: fugir para o canto
nextDir = pathfindingManager.getNextDirectionBFS(
this.x, this.y, cornerX, cornerY
);
}
if (nextDir == Direction.NONE) {
return this.direction;
}
return nextDir;
}🎯 Comportamento: Clyde é tímido e indeciso! Quando está longe do Pac-Man (> 8 tiles), ele persegue. Mas quando fica perto (≤ 8 tiles), ele foge para o canto inferior esquerdo, criando um comportamento menos ameaçador.
DFS (Depth-First Search) explora o máximo possível ao longo de cada ramo antes de retroceder, criando comportamento imprevisível.
- Complexidade: O(V + E)
- Estrutura: Stack LIFO (pilha)
- Exploração: Profunda e aleatória
- Garantia: NÃO garante caminho ótimo
public static List<Node> findPath(Node start, Node goal) {
// Pilha LIFO para DFS
Stack<Node> stack = new Stack<>();
stack.push(start);
// Conjunto de nós visitados
Set<Node> visited = new HashSet<>();
visited.add(start);
// Mapa de predecessores para reconstruir o caminho
Map<Node, Node> predecessors = new HashMap<>();
while (!stack.isEmpty()) {
Node current = stack.pop();
// Se chegamos ao objetivo, reconstruir caminho
if (current.equals(goal)) {
return reconstructPath(predecessors, start, goal);
}
// Explorar todos os vizinhos (em ordem aleatória para mais imprevisibilidade)
List<Node> neighbors = new ArrayList<>(current.getAllNeighbors());
Collections.shuffle(neighbors); // Aleatoriza a exploração
for (Node neighbor : neighbors) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
predecessors.put(neighbor, current);
stack.push(neighbor);
}
}
}
return new ArrayList<>();
}@Override
protected Direction chooseDirection(int pacmanX, int pacmanY, Direction pacmanDirection) {
// Alternar entre patrulha e perseguição a cada PATROL_DURATION frames
patrolTimer++;
if (patrolTimer >= PATROL_DURATION) {
isPatrolling = !isPatrolling;
patrolTimer = 0;
}
Direction nextDir;
if (isPatrolling) {
// Modo patrulha: escolher um alvo aleatório no mapa
int randomX = random.nextInt(19) * tileSize;
int randomY = random.nextInt(21) * tileSize;
nextDir = pathfindingManager.getNextDirectionDFS(
this.x, this.y, randomX, randomY
);
} else {
// Modo perseguição: usar DFS para seguir o Pacman
nextDir = pathfindingManager.getNextDirectionDFS(
this.x, this.y, pacmanX, pacmanY
);
}
if (nextDir == Direction.NONE) {
return this.direction;
}
return nextDir;
}🎯 Comportamento: Inky é imprevisível e errático! Ele alterna entre patrulhamento aleatório (explorando o mapa) e perseguição usando DFS, criando movimentos difíceis de prever e cobrindo áreas do labirinto de forma não-ótima.
O PathfindingManager atua como uma Facade (fachada), simplificando o uso dos algoritmos de pathfinding:
public class PathfindingManager {
private final Graph graph;
// Métodos simplificados para cada algoritmo
public Direction getNextDirectionDijkstra(int startX, int startY, int goalX, int goalY) {
return getNextDirection(startX, startY, goalX, goalY, PathAlgorithm.DIJKSTRA);
}
public Direction getNextDirectionAStar(int startX, int startY, int goalX, int goalY) {
return getNextDirection(startX, startY, goalX, goalY, PathAlgorithm.ASTAR);
}
public Direction getNextDirectionBFS(int startX, int startY, int goalX, int goalY) {
return getNextDirection(startX, startY, goalX, goalY, PathAlgorithm.BFS);
}
public Direction getNextDirectionDFS(int startX, int startY, int goalX, int goalY) {
return getNextDirection(startX, startY, goalX, goalY, PathAlgorithm.DFS);
}
// Método genérico que executa o algoritmo e retorna a próxima direção
private Direction getNextDirection(int startX, int startY, int goalX, int goalY,
PathAlgorithm algorithm) {
Node start = graph.getNode(startX, startY);
Node goal = graph.getNode(goalX, goalY);
if (start == null || goal == null) return Direction.NONE;
if (start.equals(goal)) return Direction.NONE;
// Encontrar caminho usando o algoritmo especificado
List<Node> path;
switch (algorithm) {
case DIJKSTRA: path = Dijkstra.findPath(start, goal); break;
case ASTAR: path = AStar.findPath(start, goal); break;
case BFS: path = BFS.findPath(start, goal); break;
case DFS: path = DFS.findPath(start, goal); break;
default: return Direction.NONE;
}
if (path.isEmpty() || path.size() < 2) return Direction.NONE;
// O próximo nó é o segundo elemento do caminho (índice 1)
Node nextNode = path.get(1);
// Determinar a direção para o próximo nó
return start.getDirectionTo(nextNode);
}
}Vantagens do Facade:
- Interface simplificada para os fantasmas
- Encapsula a complexidade dos algoritmos
- Facilita manutenção e testes
- Converte automaticamente caminhos em direções
O enum Direction representa as 4 direções possíveis de movimento no jogo:
public enum Direction {
UP(0, -1, 'U'),
DOWN(0, 1, 'D'),
LEFT(-1, 0, 'L'),
RIGHT(1, 0, 'R'),
NONE(0, 0, 'N');
public final int dx; // Delta X
public final int dy; // Delta Y
public final char code; // Código de caractere
Direction(int dx, int dy, char code) {
this.dx = dx;
this.dy = dy;
this.code = code;
}
// Retorna a direção oposta
public Direction opposite() {
switch (this) {
case UP: return DOWN;
case DOWN: return UP;
case LEFT: return RIGHT;
case RIGHT: return LEFT;
default: return NONE;
}
}
}Funcionalidades:
dx, dy: Vetores de movimento (ex: UP = (0, -1))code: Código de caractere para serializaçãoopposite(): Retorna direção oposta (útil para evitar reversões)NONE: Representa ausência de movimento
- Vértices (V): ~250 células navegáveis
- Arestas (E): ~400 conexões entre células adjacentes
- Tipo: Grafo não-direcionado, pesos uniformes (peso = 1)
| Algoritmo | Complexidade Tempo | Complexidade Espaço | Garante Ótimo? | Uso no Jogo |
|---|---|---|---|---|
| Dijkstra | O((V+E) log V) | O(V) | ✅ Sim | Blinky - Perseguição direta |
| A* | O(E log V) | O(V) | ✅ Sim* | Pinky - Emboscada inteligente |
| BFS | O(V + E) | O(V) | ✅ Sim** | Clyde - Comportamento tímido |
| DFS | O(V + E) | O(V) | ❌ Não | Inky - Patrulhamento errático |
* Com heurística admissível (Manhattan)
** Para grafos não-ponderados
- FPS: 60 (16ms por frame)
- Atualização de IA: A cada 4 frames (~15 vezes por segundo)
- Tempo de pathfinding: < 1ms por fantasma
- Total de cálculos: ~60 pathfindings por segundo (4 fantasmas × 15 updates)
| Fantasma | Cor | Algoritmo | Personalidade | Estratégia | Nível de Perigo |
|---|---|---|---|---|---|
| Blinky | 🔴 Vermelho | Dijkstra | Agressivo, direto | Perseguição pelo caminho mais curto | 🔥🔥🔥 ALTO |
| Pinky | 🩷 Rosa | A* | Estratégico, astuto | Emboscada prevendo posição futura | 🔥🔥🔥 ALTO |
| Inky | 🔵 Azul | DFS | Imprevisível, errático | Patrulhamento + perseguição aleatória | 🔥🔥 MÉDIO |
| Clyde | 🟠 Laranja | BFS | Tímido, covarde | Persegue longe, foge perto | 🔥 BAIXO |
- Setas do teclado ⬆️⬇️⬅️➡️ - Mover o Pac-Man
- ESC - Sair do jogo
- 🟡 Comer todas as bolinhas brancas
- 👻 Evitar os 4 fantasmas
- 🏆 Fazer a maior pontuação possível
- Cada bolinha: 10 pontos
- Vidas iniciais: 3
- Game Over: quando perder todas as vidas
pacman/
├── src/
│ ├── graph/ # 📊 Estruturas de Grafo
│ │ ├── Graph.java # Grafo principal do labirinto
│ │ ├── Node.java # Nó/Vértice do grafo
│ │ └── algorithms/ # 🧠 Algoritmos de busca
│ │ ├── Dijkstra.java # Caminho mais curto
│ │ ├── AStar.java # Busca heurística
│ │ ├── BFS.java # Busca em largura
│ │ └── DFS.java # Busca em profundidade
│ │
│ ├── models/ # 📐 Modelos de dados
│ │ ├── Position.java # Posição (x, y)
│ │ └── Direction.java # Enum de direções
│ │
│ ├── managers/ # 🎛️ Gerenciadores
│ │ └── PathfindingManager.java # Facade para pathfinding
│ │
│ ├── entities/ # 👾 Entidades do jogo
│ │ ├── Ghost.java # Classe abstrata base
│ │ ├── Blinky.java # 🔴 Vermelho - Dijkstra
│ │ ├── Pinky.java # 🩷 Rosa - A*
│ │ ├── Inky.java # 🔵 Azul - DFS
│ │ ├── Clyde.java # 🟠 Laranja - BFS
│ │ ├── Pacman.java # 🟡 Jogador
│ │ ├── RankingManager.java # Sistema de ranking
│ │ └── ScoreEntry.java # Entrada de pontuação
│ │
│ ├── App.java # 🚀 Ponto de entrada
│ └── MainMenu.java # 📋 Menu inicial
│
├── lib/
│ └── gson-2.8.9.jar # Biblioteca JSON
│
├── Images/ # 🎨 Sprites e recursos gráficos
│
├── compile.bat # Script de compilação Windows
└── run.bat # Script de execução Windows
-
Construção do Grafo:
- O labirinto 2D é convertido em um grafo
- Cada célula navegável = 1 vértice
- Células adjacentes sem parede = aresta bidirecional
- Peso uniforme = 1 para todas as arestas
-
Pathfinding em Tempo Real:
- Algoritmos trabalham sobre o grafo para calcular caminhos
- PathfindingManager converte caminhos em direções
- Atualização a cada 4 frames para otimização
-
Integração com Gameplay:
Inicialização → Construir Grafo do Labirinto ↓ Loop 60 FPS → Mover Pac-Man (input do usuário) → Mover Fantasmas (física) → Atualizar IA (a cada 4 frames) → Detectar colisões → Renderizar frame
- Modelagem de labirinto como grafo não-direcionado
- Vértices, arestas e conectividade
- Representação por lista de adjacências
- Dijkstra: Caminho mais curto com pesos
- A:* Busca heurística informada
- BFS: Busca em largura (não-informada)
- DFS: Busca em profundidade (não-informada)
- Priority Queue: Dijkstra e A*
- Queue (FIFO): BFS
- Stack (LIFO): DFS
- HashMap: Distâncias e predecessores
- HashSet: Nós visitados
- Notação Big O
- Complexidade de tempo e espaço
- Trade-offs entre algoritmos
- Facade: PathfindingManager
- Template Method: Classe abstrata Ghost
- Enum: Direction
- ✨ 4 algoritmos diferentes implementados e funcionais
- 🎮 Comportamentos únicos para cada fantasma
- 📊 Análise comparativa de desempenho
- 🏗️ Arquitetura limpa com design patterns
- 📝 Código bem documentado e didático
- ⚡ Performance otimizada (60 FPS)
- 🎯 Aplicação prática de conceitos teóricos
- Cormen, T. H., et al. (2009). Introduction to Algorithms (3rd ed.)
- Dijkstra, E. W. (1959). A note on two problems in connexion with graphs
- Hart, P. E., et al. (1968). A Formal Basis for the Heuristic Determination of Minimum Cost Paths
- Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach (4th ed.)
Projeto Acadêmico
Disciplina: Estrutura de Dados II
Instituição: UNESP - Bacharelado em Sistemas de Informação
Ano: 2025
Alunos: Marcelo Henrique Ayala Mateus Fernandes Pereira Thales Hirose Nakandakari Gleivan Afonso Bezerra
Este é um projeto acadêmico desenvolvido para fins educacionais.
🎮 Desenvolvido com Java e ❤️ para Estrutura de Dados II
UNESP - Universidade Estadual Paulista "Júlio de Mesquita Filho"