import sys import time from MoveAble import MoveAble from Node import Node from Vec import Vec # Funktion für den Pathfinding Algorithmus, welche überprüft, ob der Parent eines Nodes neu gesetzt werden muss. def handle_node_cost_check(temp_node, open_nodes, current_node, end_node): if temp_node not in open_nodes: temp_node.parent = current_node temp_node.calculate_cost(end_node) open_nodes.append(temp_node) elif current_node.p_cost + 1 + temp_node.h_cost < temp_node.g_cost: temp_node.parent = current_node temp_node.calculate_cost(end_node) class Ghost(MoveAble): def __init__(self, start_position: Vec, start_size: Vec, cell_count, grid_cell_size, player, maze_grid, start_delay): super().__init__(start_position, start_size, 2) self.cell_count = cell_count self.start_delay = start_delay self.chase = True self.stateChange = time.time() self.grid = [] self.grid_cell_size = grid_cell_size self.player = player self.maze_grid = maze_grid self.res = [] self.generate_nodes() self.distance = 0 self.direction = None self.default_pos = start_position.get_instance() self.eaten = False def reset_position(self): self.distance = 0 self.position = self.default_pos.get_instance() def idle(self): pass def hunting(self): pass # Diese Funktion wird in jedem Update des Spiels (GameLoop) aufgerufen def run(self, afraid, relative_start_time): if time.time() - relative_start_time > self.start_delay: # Sollte der Geist von PacMan gefressen worden sein, bewegt sich dieser ins Geisterhaus zurück. if self.eaten: self.move_to_point(Vec(15, 14)) if int((self.position.x + self.grid_cell_size / 2) / self.grid_cell_size) == 15 and \ int((self.position.y + self.grid_cell_size / 2) / self.grid_cell_size) == 14: self.eaten = False else: # Sollte der Geist verängstigt / Blau sein, führt dieser seine Idle routine aus. if afraid: self.idle() else: # Hier wird anhand des aktuellen States (siehe Timer) zwischen Jagd und Idle gewechselt self.timer() if self.chase: self.hunting() else: self.idle() # Diese Funktion wechselt in Zeitabständen von 16 und 4,5 Sekunden zwischen dem Jagd- und dem Idle-Modus def timer(self): if time.time() - self.stateChange >= (16 if self.chase else 4.5): self.chase = not self.chase self.stateChange = time.time() # Diese Funktion gibt an, ob das Bild des Geistes gespiegelt werden muss. def is_mirrored(self): return self.velocity.x < 0 # Diese Funktion setzt die Kosten aller Nodes zurück (dadurch müssen keine neuen Objekte generiert werden) def clear_nodes(self): for x in range(0, self.cell_count): for y in range(0, self.cell_count): self.grid[x][y].g_cost = sys.float_info.max # Generiere für jedes Feld (Wand oder Frei oder Geist) einen Knoten, welcher Informationen wie Parent und Kosten # beinhaltet die Nodes müssen nur zu Beginn des Spiels einmal generiert werden. def generate_nodes(self): for x in range(0, self.cell_count): self.grid.append([]) for y in range(0, self.cell_count): self.grid[x].append(Node(x, y)) # Diese Funktion bewegt den Geist in Richtung eines Gegeben End Punktes v def move_to_point(self, v): if self.distance <= self.speed: # Rufe den Pathfinding Algorithmus self.res = self.find_path(v) if len(self.res) > 0: x_pos = int((self.position.x + self.grid_cell_size / 2) / self.grid_cell_size) y_pos = int((self.position.y + self.grid_cell_size / 2) / self.grid_cell_size) # Bilden den Velocity Vektor (Richtung und Geschwindigkeit), wohin der Geist sich bewegen soll self.direction = Vec(self.res[0].x - x_pos, self.res[0].y - y_pos) self.direction.mul(self.speed) self.velocity = self.direction if len(self.res) > 0: # Bestimme die Distanz zum nächsten Node (resultiert aus dem Pathfinding) um oben in der Funktion zu # bestimmen, ob das Pathfinding erneut ausgeführt werden soll self.distance = abs( self.position.distance(Vec(self.res[0].x * self.grid_cell_size, self.res[0].y * self.grid_cell_size))) # Bewege den Geist self.move() # In dieser Funktion werden alle Parents eines Nodes in eine Liste abgespeichert @staticmethod def path_node_to_list(node, start_node): result = [] # Iteratives aufbauen der Liste while node is not start_node: result.append(node) node = node.parent # Liste Invertieren, da diese noch in verkehrter Reihenfolge ist. (End -> Start, muss ja Start -> End sein) result.reverse() return result # Eine Methode, welche einen Pfad von der aktuellen Position des Geistes zu einem EndPunkt berechnet def find_path(self, end_point): # Alle Nodes werde zu Beginn zurückgesetzt bzw. ihre Kosten werden auf "unendlich" gesetzt. self.clear_nodes() # Zwei listen, in welchen die zu überprüfenden Nodes und die bereits überprüften Nodes abgespeichert werden. checked_nodes = [] open_nodes = [] # umrechnen der Positionen von 0 - bildschirmbreite, 0 - bildschirmhöhe in 0 - 30, 0 - 30 x_pos = int((self.position.x + self.grid_cell_size / 2) / self.grid_cell_size) y_pos = int((self.position.y + self.grid_cell_size / 2) / self.grid_cell_size) x_pos_end_point = end_point.x y_pos_end_point = end_point.y # Positionen gegebenenfalls (falls über die Array-Grenzen hinaus) "abschneiden" if x_pos_end_point >= 30: x_pos_end_point = 29 if x_pos_end_point < 0: x_pos_end_point = 0 # Das Start Node bestimmen und dessen Kosten auf 0 setzen (dort wo man schon ist, muss man nicht hinlaufen) start_node = self.grid[x_pos][y_pos] start_node.g_cost = 0 start_node.h_cost = 0 start_node.p_cost = 0 # Das End Node merken und das Start Node in die Liste der zu überprüfenden Nodes einfügen. # (Es wird als Erstes zum current Node) end_node = self.grid[x_pos_end_point][y_pos_end_point] current_node: Node = None open_nodes.append(start_node) # Den Algorithmus so lange ausführen, bis das aktuelle Node dem End Node entspricht (=> Weg wurde gefunden) while current_node is not end_node: # Sollte die Liste der zu überprüfenden Nodes Elemente enthalten, # wird das Node mit den aktuell geringsten Kosten zum Current Node if len(open_nodes) > 0: open_nodes.sort(key=lambda x: x.g_cost) current_node = open_nodes.pop(0) else: break # Das Aktuelle Node wurde mit diesem Schleifendurchlauf überprüft, und ist hiermit fest # (es hatten in der Liste open_nodes die geringsten Kosten) checked_nodes.append(current_node) # Hier werden alle Nachbarknoten des aktuellen Knotens überprüft und eine Funktion aufgerufen, # die gegebenenfalls den Parent dieses Nachbar Nodes umsetzt. if current_node.x + 1 < 30 and ( self.maze_grid[current_node.x + 1][current_node.y] == 0 or self.maze_grid[current_node.x + 1][ current_node.y] == 2) and \ (not self.grid[current_node.x + 1][current_node.y] in checked_nodes): # Nachbar Node vom aktuellen Node (eins nach rechts) zwischen speichern temp_node = self.grid[current_node.x + 1][current_node.y] # Hier wird der Parent des Nachbar Node gegebenenfalls umgesetzt handle_node_cost_check(temp_node, open_nodes, current_node, end_node) if current_node.x - 1 >= 0 and ( self.maze_grid[current_node.x - 1][current_node.y] == 0 or self.maze_grid[current_node.x - 1][ current_node.y] == 2) and \ (not self.grid[current_node.x - 1][current_node.y] in checked_nodes): temp_node = self.grid[current_node.x - 1][current_node.y] handle_node_cost_check(temp_node, open_nodes, current_node, end_node) if current_node.y + 1 < 30 and ( self.maze_grid[current_node.x][current_node.y + 1] == 0 or self.maze_grid[current_node.x][ current_node.y + 1] == 2) and \ (not self.grid[current_node.x][current_node.y + 1] in checked_nodes): temp_node = self.grid[current_node.x][current_node.y + 1] handle_node_cost_check(temp_node, open_nodes, current_node, end_node) if current_node.y - 1 >= 0 and ( self.maze_grid[current_node.x][current_node.y - 1] == 0 or self.maze_grid[current_node.x][ current_node.y - 1] == 2) and \ (not self.grid[current_node.x][current_node.y - 1] in checked_nodes): temp_node = self.grid[current_node.x][current_node.y - 1] handle_node_cost_check(temp_node, open_nodes, current_node, end_node) # Hier wird eine Liste in der richtigen Reihenfolge basierend auf allen Parents des End Nodes zurückgegeben return self.path_node_to_list(current_node, start_node)