diff --git a/P1/graph.py b/P1/graph.py new file mode 100644 index 0000000..4bd4eab --- /dev/null +++ b/P1/graph.py @@ -0,0 +1,88 @@ +from prettytable import PrettyTable +from pygame.event import set_keyboard_grab + +from utils import * + + +class Node: + + def __init__(self, name, x=None, y=None, state="free"): + self.parent = None + self.name = name + self.edges = [] + self.value = float('inf') # cost reaching this node + + +class Edge: + + def __init__(self, edge): + self.start = edge[0] + self.end = edge[1] + self.value = edge[2] + + +class Graph: + + def __init__(self, node_list, edges): + self.nodes = [] + for name in node_list: + self.nodes.append(Node(name)) + + for e in edges: + e = (getNode(e[0], self.nodes), getNode(e[1], self.nodes), e[2]) + + self.nodes[next((i for i, v in enumerate(self.nodes) if v.name == e[0].name), -1)].edges.append(Edge(e)) + self.nodes[next((i for i, v in enumerate(self.nodes) if v.name == e[1].name), -1)].edges.append( + Edge((e[1], e[0], e[2]))) + + def print(self): + node_list = self.nodes + + t = PrettyTable([' '] + [i.name for i in node_list]) + for node in node_list: + edge_values = ['X'] * len(node_list) + for edge in node.edges: + edge_values[next((i for i, e in enumerate(node_list) if e.name == edge.end.name), -1)] = edge.g + t.add_row([node.name] + edge_values) + print(t) + + +class Queue: + def __init__(self, type, sort_by = ''): + self.type = type + self.items = [] + self.sort_by = sort_by + + def empty(self): + return len(self.items) == 0 + + def pop(self): + if not self.empty(): + if self.type == 'LIFO': + ''' LIFO + queue = [node_0, node_1, ... , node_n] + -> pop node_n + ''' + return self.items.pop() + else: + ''' FIFO & PRIO + queue = [node_0, node_1, ... , node_n] + -> pop node_0 + ''' + return self.items.pop(0) + return None + + def push(self, node): + self.items.append(node) + ''' + queue = [node_0, node_1, ... , node_n] <- node_n+1 + ''' + if self.type == 'PRIO': + ''' + Sorting so lowest cost/ value is at [0] + queue = [node_0 < node_1 < ... < node_n < node_n+1] + ''' + if self.sort_by == '': + self.items.sort(key=lambda item: item.value) + elif self.sort_by == 'f': + self.items.sort(key=lambda item: item.f) diff --git a/P1/grid.py b/P1/grid.py new file mode 100644 index 0000000..5c21151 --- /dev/null +++ b/P1/grid.py @@ -0,0 +1,258 @@ +import pygame +import math +from graph import Queue + +# Define some colors +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +BLUE = (0, 0, 255) +GREEN = (0, 255, 0) +RED = (255, 0, 0) +ORANGE = (255, 165, 0) +GREY = (128, 128, 128) + +WIDTH = 25 +HEIGHT = 25 +MARGIN = 3 + +grid_size = 20 + + +class Field: + def __init__(self, x, y): + self.x = x + self.y = y + self.state = "free" # states: free, obstacale, start, target + + self.g = float('inf') + self.h = 0 + self.f = float('inf') + self.parent = None + + def draw(self, screen): + # state based coloring + color = WHITE + if self.state == "obstacale": + color = BLACK + elif self.state == "start": + color = BLUE + elif self.state == "target": + color = GREEN + elif self.state == "path": + color = ORANGE + elif self.state == "visited": + color = WHITE + + x_calc = (MARGIN + WIDTH) * self.x + MARGIN + y_calc = (MARGIN + HEIGHT) * (grid_size - 1 - self.y) + MARGIN # flipping + + pygame.draw.rect( + screen, + color, + [x_calc, y_calc, WIDTH, HEIGHT] + ) + + # Render the heuristic value as text + if self.state != "obstacale": # Don't display on obstacles + # Create a font object + font = pygame.font.Font(None, 16) # None means default font, 14 is the size + + # Round the heuristic value to 1 decimal place for better display + f_text = f"{self.f:.1f}" + + # Render the text + text = font.render(f_text, True, BLACK) # True for anti-aliasing, BLACK for text color + + # Calculate text position (centered in the rectangle) + text_rect = text.get_rect(center=(x_calc + WIDTH / 2, y_calc + HEIGHT / 2)) + + # Draw the text on the screen + screen.blit(text, text_rect) + + +class Grid: + def __init__(self, cols, rows): + self.cols = cols # x + self.rows = rows # y + self.grid = [] + + i = 0 + while i < cols: # col = x + col = [] + j = 0 + while j < rows: # row = y + col.append(Field(i, j)) # (x,y) + j += 1 + self.grid.append(col) + i += 1 + + self.start = None + self.target = None + + def draw(self, screen): + for col in self.grid: + for field in col: + field.draw(screen) + + def heuristic(self, field): + return math.sqrt((field.x - self.target[0]) ** 2 + (field.y - self.target[1]) ** 2) + + def get_state(self, x, y): + return self.grid[x][y].state + + def set_state(self, state, x, y): + if state == "free" or state == "obstacale" or state == "start" or state == "target" or state == "path" or state == "visited": + self.grid[x][y].state = state + + def set_free(self, x, y): + self.set_state("free", x, y) + + def set_obstacle(self, x, y): + self.set_state("obstacale", x, y) + + def set_path(self, x, y): + if not (x == self.start[0] and y == self.start[1]) and not (x == self.target[0] and y == self.target[1]): + self.set_state("path", x, y) + + def set_visited(self, x, y): + if not (x == self.start[0] and y == self.start[1]) and not (x == self.target[0] and y == self.target[1]): + self.set_state("visited", x, y) + + def set_start(self, x, y): + # reset old start if it exits + if self.start: + self.set_free(self.start[0], self.start[1]) + + self.set_state("start", x, y) + self.grid[x][y].parent = self.grid[x][y] + self.grid[x][y].g = 0 + self.grid[x][y].h = self.heuristic(self.grid[x][y]) + + self.start = (x, y) + + def set_target(self, x, y): + # reset old target if it exits + if self.target: + self.set_free(self.target[0], self.target[1]) + + self.set_state("target", x, y) + + self.target = (x, y) + + +''' +Initializing the Grid +''' +start = (0, 0) +target = (19, 19) +grid = Grid(grid_size, grid_size) + +# check if start an target are valid +if 0 <= start[0] < grid.cols and 0 <= target[0] < grid.cols and 0 <= start[1] < grid.cols and 0 <= target[ + 1] < grid.cols: + grid.set_target(target[0], target[1]) + grid.set_start(start[0], start[1]) + +for i in range(0, 10): + grid.set_obstacle(9, i) + +for j in range(4, 10): + grid.set_obstacle(j, 9) + +for i in range(9, 20): + grid.set_obstacle(16, i) + +''' +Initializing A* Comps +''' + +open = Queue('PRIO', 'f') +open.push(grid.grid[0][0]) +closed = Queue('PRIO', 'f') +neighbors = [] +path = [] + +pygame.init() + +# window +window_width = grid_size * (WIDTH + MARGIN) + MARGIN +window_height = grid_size * (HEIGHT + MARGIN) + MARGIN +size = (window_width, window_height) # made size variable +screen = pygame.display.set_mode(size) + +pygame.display.set_caption("A* Algorithm") + +done = False +clock = pygame.time.Clock() + + +def a_star(): + neighbor = None + while not open.empty(): + current_field = open.pop() + + if current_field.x == grid.target[0] and current_field.y == grid.target[1]: + path.append(current_field) + grid.set_path(current_field.x, current_field.y) + while not (current_field.x == grid.start[0] and current_field.y == grid.start[1]): + current_field = current_field.parent + path.insert(0, current_field) + grid.set_path(current_field.x, current_field.y) + + break + + closed.push(current_field) + grid.set_visited(current_field.x, current_field.y) + + # Nachbarn finden + for dx, dy in [(0, -1), (-1, 0), (0, 1), (1, 0)]: + nx = current_field.x + dx + ny = current_field.y + dy + + # Prüfen, ob der Nachbar gültig ist + if 0 <= nx < grid.cols and 0 <= ny < grid.rows: + neighbor = grid.grid[nx][ny] + + # Hindernis oder bereits in geschlossener Liste -> überspringen + if neighbor.state == "obstacale" or neighbor in [item for item in closed.items]: + continue + + # Neuen g-Wert berechnen + tentative_g = current_field.g + 1 # Kosten für einen Schritt = 1 + + # Wenn Nachbar nicht in offener Liste oder neuer Pfad besser + if neighbor not in [item for item in open.items] or tentative_g < neighbor.g: + neighbor.parent = current_field + neighbor.g = tentative_g + neighbor.h = grid.heuristic(neighbor) + neighbor.f = neighbor.g + neighbor.h + + # Knoten zur offenen Liste hinzufügen oder aktualisieren + if neighbor not in [item for item in open.items]: + open.push(neighbor) + else: + # Queue aktualisieren + open.items.sort(key=lambda item: item.f) + + +def a_star_main(): + global done + while not done: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + done = True + + a_star() + screen.fill(BLACK) + grid.draw(screen) + + # refresh display + pygame.display.flip() + + # refreshrate + clock.tick(120) + + +a_star_main() + +pygame.quit() diff --git a/P1/main.py b/P1/main.py new file mode 100644 index 0000000..80bdb5c --- /dev/null +++ b/P1/main.py @@ -0,0 +1,41 @@ +from grid import a_star_main +from search import ucs, bfs, dfs +from graph import Graph + +# directed and weighted digraph +romania = Graph(['Or', 'Ne', 'Ze', 'Ia', 'Ar', 'Si', 'Fa', + 'Va', 'Ri', 'Ti', 'Lu', 'Pi', 'Ur', 'Hi', + 'Me', 'Bu', 'Dr', 'Ef', 'Cr', 'Gi'], + [ + ('Or', 'Ze', 71), ('Or', 'Si', 151), + ('Ne', 'Ia', 87), ('Ze', 'Ar', 75), + ('Ia', 'Va', 92), ('Ar', 'Si', 140), + ('Ar', 'Ti', 118), ('Si', 'Fa', 99), + ('Si', 'Ri', 80), ('Fa', 'Bu', 211), + ('Va', 'Ur', 142), ('Ri', 'Pi', 97), + ('Ri', 'Cr', 146), ('Ti', 'Lu', 111), + ('Lu', 'Me', 70), ('Me', 'Dr', 75), + ('Dr', 'Cr', 120), ('Cr', 'Pi', 138), + ('Pi', 'Bu', 101), ('Bu', 'Gi', 90), + ('Bu', 'Ur', 85), ('Ur', 'Hi', 98), + ('Hi', 'Ef', 86) + ]) + + +def main(): + # Task 1 + graph = romania + ucs(graph, 'Si', 'Bu') + + graph = romania + bfs(graph, 'Si', 'Bu') + + graph = romania + dfs(graph, 'Si', 'Bu') + + # Task 3 A* + a_star_main() + + +if __name__ == "__main__": + main() diff --git a/P1/search.py b/P1/search.py new file mode 100644 index 0000000..28d4f05 --- /dev/null +++ b/P1/search.py @@ -0,0 +1,67 @@ +from graph import * +from utils import getNode + + +def traverse(graph, frontier, start_node_name, target_node_name): + explored = [] + path = [] + + # node + start_node = getNode(start_node_name, graph.nodes) + start_node.value = 0 + + target_node = getNode(target_node_name, graph.nodes) + + frontier.push(start_node) + + while not frontier.empty(): + current_node = frontier.pop() + + if not current_node == target_node: + explored.append(current_node.name) + + for edge in current_node.edges: + child = edge.end + new_cost = current_node.value + edge.value + + if not explored.__contains__(child.name): + child.parent = current_node + child.value = new_cost + frontier.push(child) + + # UCS-only, updating the value and parent of node in the queue + elif frontier.type == 'PRIO' and new_cost < child.value: + for node in frontier.items: + if node.name == child.name: + node.value = new_cost + node.parent = current_node + frontier.items.sort(key=lambda item: item.value) + break + + else: + path.append(current_node.name) + while not current_node == start_node: + current_node = current_node.parent + path.insert(0, current_node.name) + break + + if len(path) == 0: + print('zwischen ' + start_node_name + ' und ' + target_node_name + ' konnte kein Pfad gefunden werden') + else: + print('From ' + start_node_name + ' to ' + target_node_name + ': ') + print('Path: ' + path.__str__().format()) + print('Cost: ' + target_node.value.__str__()) + + +def bfs(graph, start_node_name, target_node_name): + traverse(graph, Queue('FIFO'), start_node_name, target_node_name) + + +def dfs(graph, start_node_name, target_node_name): + traverse(graph, Queue('LIFO'), start_node_name, target_node_name) + + +def ucs(graph, start_node_name, target_node_name): + traverse(graph, Queue('PRIO'), start_node_name, target_node_name) + + diff --git a/P1/utils.py b/P1/utils.py new file mode 100644 index 0000000..bd37340 --- /dev/null +++ b/P1/utils.py @@ -0,0 +1,47 @@ +def getNode(name, l): + return next((i for i in l if i.name == name), -1) + + +def print_grid_direct(self, path=None): + """ +Gibt das Grid in der Konsole aus, direkt wie es im Array gespeichert ist. +Die y-Achse nimmt nach unten zu (0 ist oben), entsprechend der Array-Struktur. +Optionaler Parameter 'path' ist eine Liste von (x,y)-Koordinaten, die den Pfad darstellen. + """ + # Symbole für verschiedene Zustände + symbols = { + "free": ".", + "obstacale": "#", # Tippfehler aus Original-Code beibehalten + "start": "S", + "target": "G", + "path": "o" + } + + # Ausgabe des Grids mit Koordinatenachsen + print("\n ", end="") + # Obere x-Achsen-Beschriftung + for x in range(self.cols): + print(f"{x:2d}", end=" ") + print("\n " + "-" * (self.cols * 3)) + + # Grid mit y-Achsen-Beschriftung + for y in range(self.rows): + print(f"{y:2d} |", end=" ") + for x in range(self.cols): + field = self.grid[x][y] + if path and (x, y) in path and field.state != "start" and field.state != "target": + print(f"{symbols['path']:2s}", end=" ") + else: + print(f"{symbols[field.state]:2s}", end=" ") + print() # Zeilenumbruch + + print("\nLegende:") + print(" . = frei") + print(" # = Hindernis") + print(" S = Start") + print(" G = Ziel") + print(" o = Pfad") + + # Zusätzliche Statistiken, falls ein Pfad vorhanden ist + if path: + print(f"Pfadlänge: {len(path)} Felder") \ No newline at end of file diff --git a/graph.py b/graph.py deleted file mode 100644 index 5c5d1ed..0000000 --- a/graph.py +++ /dev/null @@ -1,65 +0,0 @@ -from prettytable import PrettyTable -from utils import * - -class Node: - - def __init__(self, name): - self.parent = None - self.name = name - self.edges = [] - self.value = 0 # cost reaching this node - -class Edge: - - def __init__(self, edge): - self.start = edge[0] - self.end = edge[1] - self.value = edge[2] - - -class Graph: - - def __init__(self, node_list, edges): - self.nodes = [] - for name in node_list: - self.nodes.append(Node(name)) - - for e in edges: - e = (getNode(e[0],self.nodes), getNode(e[1], self.nodes), e[2]) - - self.nodes[next((i for i,v in enumerate(self.nodes) if v.name == e[0].name), -1)].edges.append(Edge(e)) - self.nodes[next((i for i,v in enumerate(self.nodes) if v.name == e[1].name), -1)].edges.append(Edge((e[1], e[0], e[2]))) - - - def print(self): - node_list = self.nodes - - t = PrettyTable([' '] +[i.name for i in node_list]) - for node in node_list: - edge_values = ['X'] * len(node_list) - for edge in node.edges: - edge_values[ next((i for i,e in enumerate(node_list) if e.name == edge.end.name) , -1)] = edge.value - t.add_row([node.name] + edge_values) - print(t) - -class Queue: - def __init__(self, type): - self.type = type - self.items = [] - - def empty(self): - return len(self.items) == 0 - - def pop(self): - if not self.empty(): - if self.type=='FIFO': - return self.items.pop(0) - else: - return self.items.pop() - return None - - def push(self, node): - self.items.append(node) - if self.type=='PRIO': - # Sorting reverse, because nodes with lowest cost/ value should be prioritized - self.items.sort(key = lambda item: item.value, reverse=True) \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index a6c3c2a..0000000 --- a/main.py +++ /dev/null @@ -1,83 +0,0 @@ -from graph import Graph, Node, Queue -from utils import getNode - -# directed and weighted digraph -romania = Graph( ['Or', 'Ne', 'Ze', 'Ia', 'Ar', 'Si', 'Fa', - 'Va', 'Ri', 'Ti', 'Lu', 'Pi', 'Ur', 'Hi', - 'Me', 'Bu', 'Dr', 'Ef', 'Cr', 'Gi'], - [ - ('Or', 'Ze', 71), ('Or', 'Si', 151), - ('Ne', 'Ia', 87), ('Ze', 'Ar', 75), - ('Ia', 'Va', 92), ('Ar', 'Si', 140), - ('Ar', 'Ti', 118), ('Si', 'Fa', 99), - ('Si', 'Ri', 80), ('Fa', 'Bu', 211), - ('Va', 'Ur', 142), ('Ri', 'Pi', 97), - ('Ri', 'Cr', 146), ('Ti', 'Lu', 111), - ('Lu', 'Me', 70), ('Me', 'Dr', 75), - ('Dr', 'Cr', 120), ('Cr', 'Pi', 138), - ('Pi', 'Bu', 101), ('Bu', 'Gi', 90), - ('Bu', 'Ur', 85), ('Ur', 'Hi', 98), - ('Hi', 'Ef', 86) - ] ) - -def search(graph, queue, start_node_name, target_node_name): - visited_nodes = [] # Nodes which have been visited - path = [] - - start_node = getNode(start_node_name, graph.nodes) - target_node = getNode(target_node_name, graph.nodes) - - start_node.value = 0 - - queue.push(start_node) - - while not queue.empty(): - current_node = queue.pop() - visited_nodes.append(current_node.name) - - if not current_node == target_node: - for edge in current_node.edges: - neighbor = edge.end - condition = not visited_nodes.__contains__(neighbor.name) - new_cost = current_node.value + edge.value - - # UCS - if queue.type == 'PRIO': - condition = not visited_nodes.__contains__(neighbor.name) and new_cost < neighbor.value - - # works with digraph, because current_node is marked at this point, a cycle is not possible - if condition: - neighbor.parent = current_node - neighbor.value = new_cost - queue.push(neighbor) - else: - path.append(current_node.name) - while not current_node == start_node: - current_node = current_node.parent - path.insert(0, current_node.name) - - - if path.__len__() == 0: - print('zwischen ' + start_node_name + ' und ' + target_node_name + ' konnte kein Pfad gefunden werden') - else: - print('From ' + start_node_name + ' to ' + target_node_name + ': ') - print('Path: ' + path.__str__().format()) - print('Cost: ' + target_node.value.__str__()) - -def bfs(graph, start_node_name, target_node_name): - search(graph,Queue('FIFO'),start_node_name, target_node_name) - -def dfs(graph, start_node_name, target_node_name): - search(graph,Queue('LIFO'),start_node_name, target_node_name) - -def utc(graph, start_node_name, target_node_name): - search(graph,Queue('PRIO'),start_node_name, target_node_name) - - -def main(): - bfs(romania, 'Ti', 'Bu') - dfs(romania, 'Ti', 'Bu') - utc(romania, 'Or', 'Si') - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/utils.py b/utils.py deleted file mode 100644 index 784bb2e..0000000 --- a/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def getNode(name, l): - return next(( i for i in l if i.name == name), -1 ) \ No newline at end of file