| | import sys |
| | import heapq |
| | import math |
| | from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, |
| | QHBoxLayout, QPushButton, QLabel, QSpinBox, |
| | QComboBox, QMessageBox, QFrame) |
| | from PyQt5.QtCore import Qt, QTimer, pyqtSignal |
| | from PyQt5.QtGui import QPainter, QColor, QPen, QFont |
| |
|
| | class Node: |
| | def __init__(self, x, y, walkable=True): |
| | self.x = x |
| | self.y = y |
| | self.walkable = walkable |
| | self.g = 0 |
| | self.h = 0 |
| | self.f = 0 |
| | self.parent = None |
| | |
| | def __lt__(self, other): |
| | return self.f < other.f |
| |
|
| | class AStarGame(QMainWindow): |
| | def __init__(self): |
| | super().__init__() |
| | self.grid_size = 20 |
| | self.cell_size = 30 |
| | self.grid = [] |
| | self.start_node = None |
| | self.end_node = None |
| | self.path = [] |
| | self.open_set = [] |
| | self.closed_set = set() |
| | self.is_running = False |
| | self.is_paused = False |
| | self.speed = 100 |
| | self.timer = QTimer() |
| | self.timer.timeout.connect(self.step_algorithm) |
| | self.current_mode = "start" |
| | self.init_ui() |
| | self.init_grid() |
| | |
| | def init_ui(self): |
| | self.setWindowTitle("A* Pathfinding Algorithm Game") |
| | self.setFixedSize(self.grid_size * self.cell_size + 250, |
| | self.grid_size * self.cell_size + 50) |
| | |
| | |
| | central_widget = QWidget() |
| | self.setCentralWidget(central_widget) |
| | |
| | |
| | main_layout = QHBoxLayout() |
| | central_widget.setLayout(main_layout) |
| | |
| | |
| | self.grid_widget = GridWidget(self) |
| | main_layout.addWidget(self.grid_widget) |
| | |
| | |
| | control_panel = QFrame() |
| | control_panel.setFrameStyle(QFrame.Box) |
| | control_panel.setFixedWidth(200) |
| | control_layout = QVBoxLayout() |
| | control_panel.setLayout(control_layout) |
| | |
| | |
| | mode_label = QLabel("Mode:") |
| | mode_label.setFont(QFont("Arial", 12, QFont.Bold)) |
| | control_layout.addWidget(mode_label) |
| | |
| | self.mode_combo = QComboBox() |
| | self.mode_combo.addItems(["Start Point", "End Point", "Obstacles", "Erase"]) |
| | self.mode_combo.currentIndexChanged.connect(self.change_mode) |
| | control_layout.addWidget(self.mode_combo) |
| | |
| | |
| | control_layout.addSpacing(20) |
| | algo_label = QLabel("Algorithm Controls:") |
| | algo_label.setFont(QFont("Arial", 12, QFont.Bold)) |
| | control_layout.addWidget(algo_label) |
| | |
| | self.start_button = QPushButton("Start") |
| | self.start_button.clicked.connect(self.start_algorithm) |
| | control_layout.addWidget(self.start_button) |
| | |
| | self.pause_button = QPushButton("Pause") |
| | self.pause_button.clicked.connect(self.pause_algorithm) |
| | self.pause_button.setEnabled(False) |
| | control_layout.addWidget(self.pause_button) |
| | |
| | self.reset_button = QPushButton("Reset") |
| | self.reset_button.clicked.connect(self.reset_grid) |
| | control_layout.addWidget(self.reset_button) |
| | |
| | self.clear_button = QPushButton("Clear All") |
| | self.clear_button.clicked.connect(self.clear_all) |
| | control_layout.addWidget(self.clear_button) |
| | |
| | |
| | control_layout.addSpacing(20) |
| | speed_label = QLabel("Speed:") |
| | speed_label.setFont(QFont("Arial", 12, QFont.Bold)) |
| | control_layout.addWidget(speed_label) |
| | |
| | speed_layout = QHBoxLayout() |
| | self.speed_spin = QSpinBox() |
| | self.speed_spin.setRange(10, 500) |
| | self.speed_spin.setValue(self.speed) |
| | self.speed_spin.valueChanged.connect(self.change_speed) |
| | speed_layout.addWidget(self.speed_spin) |
| | speed_layout.addWidget(QLabel("ms")) |
| | control_layout.addLayout(speed_layout) |
| | |
| | |
| | control_layout.addSpacing(20) |
| | instructions_label = QLabel("Instructions:") |
| | instructions_label.setFont(QFont("Arial", 12, QFont.Bold)) |
| | control_layout.addWidget(instructions_label) |
| | |
| | instructions = QLabel( |
| | "1. Set start and end points\n" |
| | "2. Add obstacles\n" |
| | "3. Click Start to find path\n" |
| | "4. Watch the algorithm work!" |
| | ) |
| | instructions.setWordWrap(True) |
| | control_layout.addWidget(instructions) |
| | |
| | |
| | control_layout.addSpacing(20) |
| | self.status_label = QLabel("Status: Ready") |
| | self.status_label.setFont(QFont("Arial", 10)) |
| | control_layout.addWidget(self.status_label) |
| | |
| | control_layout.addStretch() |
| | main_layout.addWidget(control_panel) |
| | |
| | def init_grid(self): |
| | self.grid = [] |
| | for y in range(self.grid_size): |
| | row = [] |
| | for x in range(self.grid_size): |
| | row.append(Node(x, y)) |
| | self.grid.append(row) |
| | |
| | |
| | self.start_node = self.grid[2][2] |
| | self.end_node = self.grid[self.grid_size-3][self.grid_size-3] |
| | self.update() |
| | |
| | def change_mode(self, index): |
| | modes = ["start", "end", "obstacle", "erase"] |
| | self.current_mode = modes[index] |
| | |
| | def change_speed(self, value): |
| | self.speed = value |
| | if self.timer.isActive(): |
| | self.timer.setInterval(self.speed) |
| | |
| | def start_algorithm(self): |
| | if not self.start_node or not self.end_node: |
| | QMessageBox.warning(self, "Warning", "Please set both start and end points.") |
| | return |
| | |
| | self.is_running = True |
| | self.is_paused = False |
| | self.path = [] |
| | self.open_set = [] |
| | self.closed_set = set() |
| | |
| | |
| | self.start_node.g = 0 |
| | self.start_node.h = self.heuristic(self.start_node, self.end_node) |
| | self.start_node.f = self.start_node.g + self.start_node.h |
| | |
| | heapq.heappush(self.open_set, (self.start_node.f, self.start_node)) |
| | |
| | self.start_button.setEnabled(False) |
| | self.pause_button.setEnabled(True) |
| | self.reset_button.setEnabled(False) |
| | self.status_label.setText("Status: Running") |
| | |
| | self.timer.start(self.speed) |
| | |
| | def pause_algorithm(self): |
| | if self.is_running: |
| | if self.is_paused: |
| | self.timer.start(self.speed) |
| | self.pause_button.setText("Pause") |
| | self.status_label.setText("Status: Running") |
| | else: |
| | self.timer.stop() |
| | self.pause_button.setText("Resume") |
| | self.status_label.setText("Status: Paused") |
| | self.is_paused = not self.is_paused |
| | |
| | def reset_grid(self): |
| | self.timer.stop() |
| | self.is_running = False |
| | self.is_paused = False |
| | |
| | for row in self.grid: |
| | for node in row: |
| | node.g = 0 |
| | node.h = 0 |
| | node.f = 0 |
| | node.parent = None |
| | |
| | self.path = [] |
| | self.open_set = [] |
| | self.closed_set = set() |
| | |
| | self.start_button.setEnabled(True) |
| | self.pause_button.setEnabled(False) |
| | self.reset_button.setEnabled(True) |
| | self.pause_button.setText("Pause") |
| | self.status_label.setText("Status: Ready") |
| | |
| | self.update() |
| | |
| | def clear_all(self): |
| | self.reset_grid() |
| | self.init_grid() |
| | |
| | def step_algorithm(self): |
| | if not self.open_set: |
| | self.timer.stop() |
| | self.is_running = False |
| | self.status_label.setText("Status: No path found!") |
| | self.start_button.setEnabled(True) |
| | self.pause_button.setEnabled(False) |
| | self.reset_button.setEnabled(True) |
| | return |
| | |
| | |
| | current = heapq.heappop(self.open_set)[1] |
| | |
| | |
| | if current == self.end_node: |
| | self.timer.stop() |
| | self.is_running = False |
| | self.reconstruct_path(current) |
| | self.status_label.setText("Status: Path found!") |
| | self.start_button.setEnabled(True) |
| | self.pause_button.setEnabled(False) |
| | self.reset_button.setEnabled(True) |
| | return |
| | |
| | self.closed_set.add(current) |
| | |
| | |
| | for neighbor in self.get_neighbors(current): |
| | if neighbor in self.closed_set or not neighbor.walkable: |
| | continue |
| | |
| | tentative_g = current.g + self.distance(current, neighbor) |
| | |
| | if tentative_g < neighbor.g or neighbor not in [n[1] for n in self.open_set]: |
| | neighbor.parent = current |
| | neighbor.g = tentative_g |
| | neighbor.h = self.heuristic(neighbor, self.end_node) |
| | neighbor.f = neighbor.g + neighbor.h |
| | |
| | if neighbor not in [n[1] for n in self.open_set]: |
| | heapq.heappush(self.open_set, (neighbor.f, neighbor)) |
| | |
| | self.update() |
| | |
| | def get_neighbors(self, node): |
| | neighbors = [] |
| | for dx, dy in [(0, -1), (1, 0), (0, 1), (-1, 0), (-1, -1), (1, -1), (-1, 1), (1, 1)]: |
| | x, y = node.x + dx, node.y + dy |
| | if 0 <= x < self.grid_size and 0 <= y < self.grid_size: |
| | neighbors.append(self.grid[y][x]) |
| | return neighbors |
| | |
| | def distance(self, node_a, node_b): |
| | |
| | dx = node_a.x - node_b.x |
| | dy = node_a.y - node_b.y |
| | return math.sqrt(dx*dx + dy*dy) |
| | |
| | def heuristic(self, node_a, node_b): |
| | |
| | return abs(node_a.x - node_b.x) + abs(node_a.y - node_b.y) |
| | |
| | def reconstruct_path(self, current): |
| | self.path = [] |
| | while current: |
| | self.path.append(current) |
| | current = current.parent |
| | self.path.reverse() |
| | |
| | def handle_click(self, x, y): |
| | if self.is_running: |
| | return |
| | |
| | node = self.grid[y][x] |
| | |
| | if self.current_mode == "start": |
| | if node != self.end_node and node.walkable: |
| | self.start_node = node |
| | elif self.current_mode == "end": |
| | if node != self.start_node and node.walkable: |
| | self.end_node = node |
| | elif self.current_mode == "obstacle": |
| | if node != self.start_node and node != self.end_node: |
| | node.walkable = False |
| | elif self.current_mode == "erase": |
| | node.walkable = True |
| | if node == self.start_node: |
| | self.start_node = None |
| | elif node == self.end_node: |
| | self.end_node = None |
| | |
| | self.update() |
| |
|
| | class GridWidget(QWidget): |
| | def __init__(self, game): |
| | super().__init__() |
| | self.game = game |
| | self.setMouseTracking(True) |
| | |
| | def mousePressEvent(self, event): |
| | if event.button() == Qt.LeftButton: |
| | x = event.x() // self.game.cell_size |
| | y = event.y() // self.game.cell_size |
| | if 0 <= x < self.game.grid_size and 0 <= y < self.game.grid_size: |
| | self.game.handle_click(x, y) |
| | |
| | def paintEvent(self, event): |
| | painter = QPainter(self) |
| | painter.setRenderHint(QPainter.Antialiasing) |
| | |
| | |
| | for y in range(self.game.grid_size): |
| | for x in range(self.game.grid_size): |
| | node = self.game.grid[y][x] |
| | rect = (x * self.game.cell_size, y * self.game.cell_size, |
| | self.game.cell_size, self.game.cell_size) |
| | |
| | |
| | if node == self.game.start_node: |
| | painter.fillRect(*rect, QColor(0, 200, 0)) |
| | elif node == self.game.end_node: |
| | painter.fillRect(*rect, QColor(200, 0, 0)) |
| | elif not node.walkable: |
| | painter.fillRect(*rect, QColor(100, 100, 100)) |
| | elif node in self.game.path: |
| | painter.fillRect(*rect, QColor(0, 0, 200)) |
| | elif node in [n[1] for n in self.game.open_set]: |
| | painter.fillRect(*rect, QColor(200, 200, 100)) |
| | elif node in self.game.closed_set: |
| | painter.fillRect(*rect, QColor(200, 150, 150)) |
| | else: |
| | painter.fillRect(*rect, QColor(255, 255, 255)) |
| | |
| | |
| | painter.setPen(QPen(QColor(200, 200, 200), 1)) |
| | painter.drawRect(*rect) |
| | |
| | |
| | if self.game.is_running and node.g > 0: |
| | painter.setPen(QPen(QColor(0, 0, 0), 1)) |
| | painter.setFont(QFont("Arial", 8)) |
| | painter.drawText(rect[0] + 2, rect[1] + 12, f"g:{node.g:.1f}") |
| | painter.drawText(rect[0] + 2, rect[1] + 24, f"h:{node.h:.1f}") |
| |
|
| | def main(): |
| | app = QApplication(sys.argv) |
| | game = AStarGame() |
| | game.show() |
| | sys.exit(app.exec_()) |
| |
|
| | if __name__ == "__main__": |
| | main() |