|
|
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() |