En este tema, exploraremos los algoritmos de búsqueda de caminos, fundamentales para la navegación en videojuegos. Estos algoritmos permiten a los personajes del juego (NPCs) encontrar rutas óptimas desde un punto de inicio hasta un destino, evitando obstáculos y optimizando el tiempo de recorrido.

Conceptos Clave

  1. Grafo: Representación de un espacio de navegación donde los nodos son puntos y las aristas son caminos entre esos puntos.
  2. Nodo: Un punto en el grafo que representa una posición en el espacio de navegación.
  3. Arista: Una conexión entre dos nodos que representa un camino posible.
  4. Costo: Valor asociado a una arista que indica el "costo" de moverse de un nodo a otro (puede ser distancia, tiempo, etc.).
  5. Heurística: Estimación del costo restante desde un nodo hasta el destino, utilizada para guiar la búsqueda.

Algoritmos Principales

  1. Búsqueda en Anchura (Breadth-First Search, BFS)

El algoritmo BFS explora todos los nodos a una distancia determinada del nodo inicial antes de pasar a los nodos a una distancia mayor. Es útil para encontrar el camino más corto en grafos no ponderados.

Pasos del Algoritmo BFS:

  1. Colocar el nodo inicial en una cola.
  2. Marcar el nodo inicial como visitado.
  3. Mientras la cola no esté vacía:
    • Sacar el nodo al frente de la cola.
    • Para cada vecino no visitado del nodo actual:
      • Marcar el vecino como visitado.
      • Añadir el vecino a la cola.

Ejemplo en Python:

from collections import deque

def bfs(graph, start, goal):
    queue = deque([start])
    visited = set([start])
    parent = {start: None}

    while queue:
        current = queue.popleft()
        if current == goal:
            break
        for neighbor in graph[current]:
            if neighbor not in visited:
                visited.add(neighbor)
                parent[neighbor] = current
                queue.append(neighbor)
    
    # Reconstruir el camino
    path = []
    while goal is not None:
        path.append(goal)
        goal = parent[goal]
    path.reverse()
    return path

# Ejemplo de uso
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}
print(bfs(graph, 'A', 'F'))  # Output: ['A', 'C', 'F']

  1. Búsqueda en Profundidad (Depth-First Search, DFS)

El algoritmo DFS explora tan profundo como sea posible a lo largo de cada rama antes de retroceder. Es útil para explorar todas las posibilidades en un grafo.

Pasos del Algoritmo DFS:

  1. Colocar el nodo inicial en una pila.
  2. Marcar el nodo inicial como visitado.
  3. Mientras la pila no esté vacía:
    • Sacar el nodo al tope de la pila.
    • Para cada vecino no visitado del nodo actual:
      • Marcar el vecino como visitado.
      • Añadir el vecino a la pila.

Ejemplo en Python:

def dfs(graph, start, goal):
    stack = [start]
    visited = set([start])
    parent = {start: None}

    while stack:
        current = stack.pop()
        if current == goal:
            break
        for neighbor in graph[current]:
            if neighbor not in visited:
                visited.add(neighbor)
                parent[neighbor] = current
                stack.append(neighbor)
    
    # Reconstruir el camino
    path = []
    while goal is not None:
        path.append(goal)
        goal = parent[goal]
    path.reverse()
    return path

# Ejemplo de uso
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}
print(dfs(graph, 'A', 'F'))  # Output: ['A', 'C', 'F']

  1. Algoritmo de Dijkstra

El algoritmo de Dijkstra encuentra el camino más corto desde un nodo inicial a todos los demás nodos en un grafo ponderado.

Pasos del Algoritmo de Dijkstra:

  1. Inicializar la distancia de todos los nodos al infinito, excepto el nodo inicial que se establece en 0.
  2. Colocar todos los nodos en una cola de prioridad.
  3. Mientras la cola no esté vacía:
    • Extraer el nodo con la menor distancia.
    • Para cada vecino del nodo extraído:
      • Calcular la distancia tentativa.
      • Si la distancia tentativa es menor que la distancia actual:
        • Actualizar la distancia.
        • Actualizar el nodo padre.

Ejemplo en Python:

import heapq

def dijkstra(graph, start, goal):
    queue = [(0, start)]
    distances = {node: float('inf') for node in graph}
    distances[start] = 0
    parent = {start: None}
    
    while queue:
        current_distance, current_node = heapq.heappop(queue)
        
        if current_node == goal:
            break
        
        for neighbor, weight in graph[current_node].items():
            distance = current_distance + weight
            
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                parent[neighbor] = current_node
                heapq.heappush(queue, (distance, neighbor))
    
    # Reconstruir el camino
    path = []
    while goal is not None:
        path.append(goal)
        goal = parent[goal]
    path.reverse()
    return path

# Ejemplo de uso
graph = {
    'A': {'B': 1, 'C': 4},
    'B': {'A': 1, 'D': 2, 'E': 5},
    'C': {'A': 4, 'F': 3},
    'D': {'B': 2},
    'E': {'B': 5, 'F': 1},
    'F': {'C': 3, 'E': 1}
}
print(dijkstra(graph, 'A', 'F'))  # Output: ['A', 'B', 'D', 'E', 'F']

  1. Algoritmo A* (A Estrella)

El algoritmo A* es una mejora del algoritmo de Dijkstra que utiliza una heurística para guiar la búsqueda, haciendo que sea más eficiente.

Pasos del Algoritmo A*:

  1. Inicializar la distancia de todos los nodos al infinito, excepto el nodo inicial que se establece en 0.
  2. Inicializar la heurística para todos los nodos.
  3. Colocar todos los nodos en una cola de prioridad.
  4. Mientras la cola no esté vacía:
    • Extraer el nodo con la menor suma de distancia y heurística.
    • Para cada vecino del nodo extraído:
      • Calcular la distancia tentativa.
      • Si la distancia tentativa es menor que la distancia actual:
        • Actualizar la distancia.
        • Actualizar el nodo padre.

Ejemplo en Python:

import heapq

def heuristic(a, b):
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

def a_star(graph, start, goal):
    queue = [(0, start)]
    distances = {node: float('inf') for node in graph}
    distances[start] = 0
    parent = {start: None}
    
    while queue:
        current_distance, current_node = heapq.heappop(queue)
        
        if current_node == goal:
            break
        
        for neighbor, weight in graph[current_node].items():
            distance = current_distance + weight
            heuristic_cost = distance + heuristic(neighbor, goal)
            
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                parent[neighbor] = current_node
                heapq.heappush(queue, (heuristic_cost, neighbor))
    
    # Reconstruir el camino
    path = []
    while goal is not None:
        path.append(goal)
        goal = parent[goal]
    path.reverse()
    return path

# Ejemplo de uso
graph = {
    (0, 0): {(0, 1): 1, (1, 0): 1},
    (0, 1): {(0, 0): 1, (1, 1): 1},
    (1, 0): {(0, 0): 1, (1, 1): 1},
    (1, 1): {(0, 1): 1, (1, 0): 1}
}
print(a_star(graph, (0, 0), (1, 1)))  # Output: [(0, 0), (0, 1), (1, 1)]

Ejercicios Prácticos

Ejercicio 1: Implementar BFS en un Grafo

Instrucciones:

  1. Implementa el algoritmo BFS para un grafo dado.
  2. Encuentra el camino más corto desde el nodo 'A' al nodo 'F'.

Código Inicial:

def bfs(graph, start, goal):
    # Tu código aquí
    pass

# Ejemplo de uso
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}
print(bfs(graph, 'A', 'F'))  # Debería imprimir ['A', 'C', 'F']

Solución:

from collections import deque

def bfs(graph, start, goal):
    queue = deque([start])
    visited = set([start])
    parent = {start: None}

    while queue:
        current = queue.popleft()
        if current == goal:
            break
        for neighbor in graph[current]:
            if neighbor not in visited:
                visited.add(neighbor)
                parent[neighbor] = current
                queue.append(neighbor)
    
    # Reconstruir el camino
    path = []
    while goal is not None:
        path.append(goal)
        goal = parent[goal]
    path.reverse()
    return path

# Ejemplo de uso
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}
print(bfs(graph, 'A', 'F'))  # Output: ['A', 'C', 'F']

Ejercicio 2: Implementar A* en un Grafo

Instrucciones:

  1. Implementa el algoritmo A* para un grafo dado.
  2. Encuentra el camino más corto desde el nodo (0, 0) al nodo (1, 1).

Código Inicial:

def a_star(graph, start, goal):
    # Tu código aquí
    pass

# Ejemplo de uso
graph = {
    (0, 0): {(0, 1): 1, (1, 0): 1},
    (0, 1): {(0, 0): 1, (1, 1): 1},
    (1, 0): {(0, 0): 1, (1, 1): 1},
    (1, 1): {(0, 1): 1, (1, 0): 1}
}
print(a_star(graph, (0, 0), (1, 1)))  # Debería imprimir [(0, 0), (0, 1), (1, 1)]

Solución:

import heapq

def heuristic(a, b):
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

def a_star(graph, start, goal):
    queue = [(0, start)]
    distances = {node: float('inf') for node in graph}
    distances[start] = 0
    parent = {start: None}
    
    while queue:
        current_distance, current_node = heapq.heappop(queue)
        
        if current_node == goal:
            break
        
        for neighbor, weight in graph[current_node].items():
            distance = current_distance + weight
            heuristic_cost = distance + heuristic(neighbor, goal)
            
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                parent[neighbor] = current_node
                heapq.heappush(queue, (heuristic_cost, neighbor))
    
    # Reconstruir el camino
    path = []
    while goal is not None:
        path.append(goal)
        goal = parent[goal]
    path.reverse()
    return path

# Ejemplo de uso
graph = {
    (0, 0): {(0, 1): 1, (1, 0): 1},
    (0, 1): {(0, 0): 1, (1, 1): 1},
    (1, 0): {(0, 0): 1, (1, 1): 1},
    (1, 1): {(0, 1): 1, (1, 0): 1}
}
print(a_star(graph, (0, 0), (1, 1)))  # Output: [(0, 0), (0, 1), (1, 1)]

Conclusión

En esta sección, hemos cubierto varios algoritmos de búsqueda de caminos, incluyendo BFS, DFS, Dijkstra y A*. Cada uno tiene sus propias ventajas y aplicaciones dependiendo del contexto del juego y las necesidades de navegación. En el siguiente tema, profundizaremos en la implementación del algoritmo A* y cómo se puede utilizar en la navegación de personajes en videojuegos.

© Copyright 2024. Todos los derechos reservados