TroglodyteDerivations commited on
Commit
80b67bf
·
verified ·
1 Parent(s): ed69de8

Upload 47 files

Browse files
Files changed (48) hide show
  1. .gitattributes +34 -0
  2. flux_krea_00365_.png +3 -0
  3. output.mp4 +3 -0
  4. vacuum_simulation/Screenshot 2025-10-28 at 1.44.16/342/200/257PM.png +0 -0
  5. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.06.15 AM.png +3 -0
  6. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.10.05 AM.png +3 -0
  7. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.22.11 AM.png +3 -0
  8. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.22.40 AM.png +3 -0
  9. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.25.29 AM.png +3 -0
  10. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.25.52 AM.png +3 -0
  11. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.27.57 AM.png +3 -0
  12. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.29.58 AM.png +3 -0
  13. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.30.13 AM.png +3 -0
  14. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.32.24 AM.png +3 -0
  15. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.32.39 AM.png +3 -0
  16. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.34.30 AM.png +3 -0
  17. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.34.44 AM.png +3 -0
  18. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.37.29 AM.png +3 -0
  19. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.37.40 AM.png +3 -0
  20. vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.40.11 AM.png +3 -0
  21. vacuum_simulation/V0/environment.py +71 -0
  22. vacuum_simulation/V0/main.py +20 -0
  23. vacuum_simulation/V0/search_algorithms.py +178 -0
  24. vacuum_simulation/V0/vacuum_simulation.py +339 -0
  25. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.23.03 PM.png +3 -0
  26. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.24.27 PM.png +3 -0
  27. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.24.52 PM.png +3 -0
  28. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.26.11 PM.png +3 -0
  29. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.27.03 PM.png +3 -0
  30. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.28.01 PM.png +3 -0
  31. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.28.29 PM.png +3 -0
  32. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.29.33 PM.png +3 -0
  33. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.30.06 PM.png +3 -0
  34. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.31.17 PM.png +3 -0
  35. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.31.42 PM.png +3 -0
  36. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.33.19 PM.png +3 -0
  37. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.33.42 PM.png +3 -0
  38. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.34.51 PM.png +3 -0
  39. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.35.56 PM.png +3 -0
  40. vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.36.55 PM.png +3 -0
  41. vacuum_simulation/__pycache__/environment.cpython-313.pyc +0 -0
  42. vacuum_simulation/__pycache__/search_algorithms.cpython-313.pyc +0 -0
  43. vacuum_simulation/__pycache__/vacuum_simulation.cpython-313.pyc +0 -0
  44. vacuum_simulation/environment.py +89 -0
  45. vacuum_simulation/main.py +20 -0
  46. vacuum_simulation/requirements.txt +1 -0
  47. vacuum_simulation/search_algorithms.py +224 -0
  48. vacuum_simulation/vacuum_simulation.py +523 -0
.gitattributes CHANGED
@@ -33,3 +33,37 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ flux_krea_00365_.png filter=lfs diff=lfs merge=lfs -text
37
+ output.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.06.15 AM.png filter=lfs diff=lfs merge=lfs -text
39
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.10.05 AM.png filter=lfs diff=lfs merge=lfs -text
40
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.22.11 AM.png filter=lfs diff=lfs merge=lfs -text
41
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.22.40 AM.png filter=lfs diff=lfs merge=lfs -text
42
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.25.29 AM.png filter=lfs diff=lfs merge=lfs -text
43
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.25.52 AM.png filter=lfs diff=lfs merge=lfs -text
44
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.27.57 AM.png filter=lfs diff=lfs merge=lfs -text
45
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.29.58 AM.png filter=lfs diff=lfs merge=lfs -text
46
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.30.13 AM.png filter=lfs diff=lfs merge=lfs -text
47
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.32.24 AM.png filter=lfs diff=lfs merge=lfs -text
48
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.32.39 AM.png filter=lfs diff=lfs merge=lfs -text
49
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.34.30 AM.png filter=lfs diff=lfs merge=lfs -text
50
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.34.44 AM.png filter=lfs diff=lfs merge=lfs -text
51
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.37.29 AM.png filter=lfs diff=lfs merge=lfs -text
52
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.37.40 AM.png filter=lfs diff=lfs merge=lfs -text
53
+ vacuum_simulation/V0/V0[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]11.40.11 AM.png filter=lfs diff=lfs merge=lfs -text
54
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.23.03 PM.png filter=lfs diff=lfs merge=lfs -text
55
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.24.27 PM.png filter=lfs diff=lfs merge=lfs -text
56
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.24.52 PM.png filter=lfs diff=lfs merge=lfs -text
57
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.26.11 PM.png filter=lfs diff=lfs merge=lfs -text
58
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.27.03 PM.png filter=lfs diff=lfs merge=lfs -text
59
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.28.01 PM.png filter=lfs diff=lfs merge=lfs -text
60
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.28.29 PM.png filter=lfs diff=lfs merge=lfs -text
61
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.29.33 PM.png filter=lfs diff=lfs merge=lfs -text
62
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.30.06 PM.png filter=lfs diff=lfs merge=lfs -text
63
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.31.17 PM.png filter=lfs diff=lfs merge=lfs -text
64
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.31.42 PM.png filter=lfs diff=lfs merge=lfs -text
65
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.33.19 PM.png filter=lfs diff=lfs merge=lfs -text
66
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.33.42 PM.png filter=lfs diff=lfs merge=lfs -text
67
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.34.51 PM.png filter=lfs diff=lfs merge=lfs -text
68
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.35.56 PM.png filter=lfs diff=lfs merge=lfs -text
69
+ vacuum_simulation/V1/V1[[:space:]]Visualizations/Screenshot[[:space:]]2025-10-28[[:space:]]at[[:space:]]12.36.55 PM.png filter=lfs diff=lfs merge=lfs -text
flux_krea_00365_.png ADDED

Git LFS Details

  • SHA256: 9a9b8d4bfbf37d4cbf9281a66e1328b5c3356ce78149099fee4cb19d3b968522
  • Pointer size: 132 Bytes
  • Size of remote file: 1.07 MB
output.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d5c3b365a459d04e8d1b3406f51e592a1cb8796839f4b744305b15971752727
3
+ size 17767411
vacuum_simulation/Screenshot 2025-10-28 at 1.44.16/342/200/257PM.png ADDED
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.06.15 AM.png ADDED

Git LFS Details

  • SHA256: 667b34421c96f4bb6f281ca4f55631c22e5020d265de516dfa68a0149452f7a4
  • Pointer size: 131 Bytes
  • Size of remote file: 108 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.10.05 AM.png ADDED

Git LFS Details

  • SHA256: 312b08d897a2e485e0766b8cd57d4ab6f716f2dc0d6a61d34a820ccbce9bd8a7
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.22.11 AM.png ADDED

Git LFS Details

  • SHA256: 3e36450f9263f54032b85c071c6bb9b6df0695ab4fbca8f8b37b386c314547a5
  • Pointer size: 131 Bytes
  • Size of remote file: 123 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.22.40 AM.png ADDED

Git LFS Details

  • SHA256: 0f05df37a9dcf6511ec3cfe32c2e584ac5a19fd96a39d47f3009723a19686292
  • Pointer size: 131 Bytes
  • Size of remote file: 130 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.25.29 AM.png ADDED

Git LFS Details

  • SHA256: 67b886109894fb058f554fffbca523ccde8ea31a328cc1b13e57c256c0e85058
  • Pointer size: 131 Bytes
  • Size of remote file: 124 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.25.52 AM.png ADDED

Git LFS Details

  • SHA256: 1ddbbc3f378400cbadf11c0dc86b2b352f21e23544fb73a296d5cf819701eb7d
  • Pointer size: 131 Bytes
  • Size of remote file: 130 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.27.57 AM.png ADDED

Git LFS Details

  • SHA256: a41638b92af8891f49ecfe76e13b8eaf43189b2c63e4e3da38cedce6be85e373
  • Pointer size: 131 Bytes
  • Size of remote file: 127 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.29.58 AM.png ADDED

Git LFS Details

  • SHA256: 1710f76123f425284e0acda8830de1f752bd412945e16460420495fdf7f16023
  • Pointer size: 131 Bytes
  • Size of remote file: 125 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.30.13 AM.png ADDED

Git LFS Details

  • SHA256: 5ff02763b147b718bd38b46ef5f006251dccc2d0d916c9b36c8679e6d35959ef
  • Pointer size: 131 Bytes
  • Size of remote file: 130 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.32.24 AM.png ADDED

Git LFS Details

  • SHA256: 672bb5e78714a5b92beebecc74baa2c2423728f0e26dba53132807659cb31457
  • Pointer size: 131 Bytes
  • Size of remote file: 124 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.32.39 AM.png ADDED

Git LFS Details

  • SHA256: 07c45bfca964a5b9202bb3e2e203f439689e2bb60f4611aa788d471853906fcd
  • Pointer size: 131 Bytes
  • Size of remote file: 132 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.34.30 AM.png ADDED

Git LFS Details

  • SHA256: 5f0d319e366a4eb3694aa1cb15453f2878d8516f68b492f343ccd0d5b6cd74a7
  • Pointer size: 131 Bytes
  • Size of remote file: 125 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.34.44 AM.png ADDED

Git LFS Details

  • SHA256: fcd516e31e38c4617c6b85486f3e024c67f351a0e50ad7325cce8e571aa842d9
  • Pointer size: 131 Bytes
  • Size of remote file: 129 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.37.29 AM.png ADDED

Git LFS Details

  • SHA256: 3fddb2bbcb20964eb420041fb6c8af28be759132de8b98a2dd9705cea1c0cd32
  • Pointer size: 131 Bytes
  • Size of remote file: 127 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.37.40 AM.png ADDED

Git LFS Details

  • SHA256: bb838adaefd8225211afb189235d9b3964de25a8b7a6fa646038e6ae17a3fb1e
  • Pointer size: 131 Bytes
  • Size of remote file: 131 kB
vacuum_simulation/V0/V0 Visualizations/Screenshot 2025-10-28 at 11.40.11 AM.png ADDED

Git LFS Details

  • SHA256: 6ac17ee79a6498b62957d106f067aba5e4005d9a813c6b703a040bf5245bc080
  • Pointer size: 131 Bytes
  • Size of remote file: 126 kB
vacuum_simulation/V0/environment.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from enum import Enum
3
+
4
+ class CellType(Enum):
5
+ CLEAN = 0
6
+ DIRTY = 1
7
+ OBSTACLE = 2
8
+ EXPLORED = 3
9
+
10
+ class Direction(Enum):
11
+ UP = 0
12
+ RIGHT = 1
13
+ DOWN = 2
14
+ LEFT = 3
15
+
16
+ class Environment:
17
+ def __init__(self, rows, cols, obstacle_density=0.2, dirt_density=0.3):
18
+ self.rows = rows
19
+ self.cols = cols
20
+ self.grid = [[CellType.CLEAN for _ in range(cols)] for _ in range(rows)]
21
+ self.obstacle_density = obstacle_density
22
+ self.dirt_density = dirt_density
23
+ self.vacuum_pos = None
24
+ self.dirty_cells = set()
25
+
26
+ self.generate_environment()
27
+
28
+ def generate_environment(self):
29
+ # Place obstacles
30
+ for i in range(self.rows):
31
+ for j in range(self.cols):
32
+ if random.random() < self.obstacle_density:
33
+ self.grid[i][j] = CellType.OBSTACLE
34
+
35
+ # Place dirt
36
+ for i in range(self.rows):
37
+ for j in range(self.cols):
38
+ if self.grid[i][j] == CellType.CLEAN and random.random() < self.dirt_density:
39
+ self.grid[i][j] = CellType.DIRTY
40
+ self.dirty_cells.add((i, j))
41
+
42
+ # Place vacuum at a random clean position
43
+ clean_positions = [(i, j) for i in range(self.rows) for j in range(self.cols)
44
+ if self.grid[i][j] == CellType.CLEAN]
45
+ if clean_positions:
46
+ self.vacuum_pos = random.choice(clean_positions)
47
+
48
+ def reset(self):
49
+ self.grid = [[CellType.CLEAN for _ in range(self.cols)] for _ in range(self.rows)]
50
+ self.dirty_cells = set()
51
+ self.generate_environment()
52
+
53
+ def is_valid_position(self, row, col):
54
+ return 0 <= row < self.rows and 0 <= col < self.cols and self.grid[row][col] != CellType.OBSTACLE
55
+
56
+ def clean_cell(self, row, col):
57
+ if (row, col) in self.dirty_cells:
58
+ self.grid[row][col] = CellType.CLEAN
59
+ self.dirty_cells.remove((row, col))
60
+ return True
61
+ return False
62
+
63
+ def mark_explored(self, row, col):
64
+ if self.grid[row][col] == CellType.CLEAN:
65
+ self.grid[row][col] = CellType.EXPLORED
66
+
67
+ def get_dirty_count(self):
68
+ return len(self.dirty_cells)
69
+
70
+ def is_clean(self):
71
+ return len(self.dirty_cells) == 0
vacuum_simulation/V0/main.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import random
3
+ from PyQt5.QtWidgets import QApplication
4
+ from vacuum_simulation import VacuumSimulation
5
+
6
+ if __name__ == "__main__":
7
+ app = QApplication(sys.argv)
8
+
9
+ # Default grid size or use command line arguments
10
+ rows, cols = 15, 15
11
+ if len(sys.argv) >= 3:
12
+ try:
13
+ rows, cols = int(sys.argv[1]), int(sys.argv[2])
14
+ except ValueError:
15
+ print("Invalid grid dimensions. Using default 15x15.")
16
+
17
+ window = VacuumSimulation(rows, cols)
18
+ window.show()
19
+
20
+ sys.exit(app.exec_())
vacuum_simulation/V0/search_algorithms.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import heapq
2
+ import math
3
+ from collections import deque
4
+ from environment import Direction
5
+
6
+ class Node:
7
+ def __init__(self, position, parent=None, direction=None):
8
+ self.position = position
9
+ self.parent = parent
10
+ self.direction = direction
11
+ self.g = 0 # Cost from start to current node
12
+ self.h = 0 # Heuristic cost from current node to goal
13
+ self.f = 0 # Total cost (g + h)
14
+
15
+ def __eq__(self, other):
16
+ return self.position == other.position
17
+
18
+ def __lt__(self, other):
19
+ return self.f < other.f
20
+
21
+ class SearchAlgorithms:
22
+ def __init__(self, environment, turn_cost_enabled=False):
23
+ self.env = environment
24
+ self.turn_cost_enabled = turn_cost_enabled
25
+
26
+ def get_neighbors(self, position, direction=None):
27
+ row, col = position
28
+ neighbors = []
29
+
30
+ # Possible moves: up, right, down, left
31
+ moves = [(-1, 0, Direction.UP), (0, 1, Direction.RIGHT),
32
+ (1, 0, Direction.DOWN), (0, -1, Direction.LEFT)]
33
+
34
+ for dr, dc, new_dir in moves:
35
+ new_row, new_col = row + dr, col + dc
36
+ if self.env.is_valid_position(new_row, new_col):
37
+ turn_cost = 0
38
+ if self.turn_cost_enabled and direction is not None and direction != new_dir:
39
+ # Calculate turn cost (0.5 for 90° turns)
40
+ turn_cost = 0.5
41
+
42
+ neighbors.append(((new_row, new_col), new_dir, 1 + turn_cost))
43
+
44
+ return neighbors
45
+
46
+ def get_diagonal_neighbors(self, position, direction=None):
47
+ row, col = position
48
+ neighbors = []
49
+
50
+ # Possible moves including diagonals
51
+ moves = [
52
+ (-1, 0, Direction.UP, 1), (0, 1, Direction.RIGHT, 1),
53
+ (1, 0, Direction.DOWN, 1), (0, -1, Direction.LEFT, 1),
54
+ (-1, -1, None, math.sqrt(2)), (-1, 1, None, math.sqrt(2)),
55
+ (1, -1, None, math.sqrt(2)), (1, 1, None, math.sqrt(2))
56
+ ]
57
+
58
+ for dr, dc, new_dir, cost in moves:
59
+ new_row, new_col = row + dr, col + dc
60
+ if self.env.is_valid_position(new_row, new_col):
61
+ turn_cost = 0
62
+ if self.turn_cost_enabled and direction is not None and direction != new_dir and new_dir is not None:
63
+ turn_cost = 0.5
64
+
65
+ neighbors.append(((new_row, new_col), new_dir, cost + turn_cost))
66
+
67
+ return neighbors
68
+
69
+ def manhattan_distance(self, pos1, pos2):
70
+ return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1])
71
+
72
+ def euclidean_distance(self, pos1, pos2):
73
+ return math.sqrt((pos1[0] - pos2[0])**2 + (pos1[1] - pos2[1])**2)
74
+
75
+ def chebyshev_distance(self, pos1, pos2):
76
+ return max(abs(pos1[0] - pos2[0]), abs(pos1[1] - pos2[1]))
77
+
78
+ def bfs(self, start, goals):
79
+ """Breadth-First Search"""
80
+ if not goals:
81
+ return None, set()
82
+
83
+ queue = deque([Node(start)])
84
+ visited = set([start])
85
+ explored = set([start])
86
+
87
+ while queue:
88
+ current_node = queue.popleft()
89
+
90
+ # Check if we reached any goal
91
+ if current_node.position in goals:
92
+ path = []
93
+ while current_node:
94
+ path.append(current_node.position)
95
+ current_node = current_node.parent
96
+ return path[::-1], explored
97
+
98
+ for neighbor, _, _ in self.get_neighbors(current_node.position):
99
+ if neighbor not in visited:
100
+ visited.add(neighbor)
101
+ explored.add(neighbor)
102
+ queue.append(Node(neighbor, current_node))
103
+
104
+ return None, explored
105
+
106
+ def a_star(self, start, goals, heuristic_type="manhattan", allow_diagonals=False):
107
+ """A* Search with different heuristics"""
108
+ if not goals:
109
+ return None, set()
110
+
111
+ open_list = []
112
+ heapq.heappush(open_list, Node(start))
113
+ closed_list = set()
114
+ explored = set([start])
115
+
116
+ # Cost from start to node
117
+ g_costs = {start: 0}
118
+
119
+ while open_list:
120
+ current_node = heapq.heappop(open_list)
121
+
122
+ # Check if we reached any goal
123
+ if current_node.position in goals:
124
+ path = []
125
+ while current_node:
126
+ path.append(current_node.position)
127
+ current_node = current_node.parent
128
+ return path[::-1], explored
129
+
130
+ closed_list.add(current_node.position)
131
+
132
+ # Get neighbors based on whether diagonals are allowed
133
+ if allow_diagonals:
134
+ neighbors = self.get_diagonal_neighbors(current_node.position, current_node.direction)
135
+ else:
136
+ neighbors = self.get_neighbors(current_node.position, current_node.direction)
137
+
138
+ for neighbor_pos, direction, move_cost in neighbors:
139
+ if neighbor_pos in closed_list:
140
+ continue
141
+
142
+ # Calculate new g cost
143
+ new_g = g_costs[current_node.position] + move_cost
144
+
145
+ if neighbor_pos not in g_costs or new_g < g_costs[neighbor_pos]:
146
+ # Calculate heuristic to the closest goal
147
+ min_h = float('inf')
148
+ for goal in goals:
149
+ if heuristic_type == "manhattan":
150
+ h = self.manhattan_distance(neighbor_pos, goal)
151
+ elif heuristic_type == "euclidean":
152
+ h = self.euclidean_distance(neighbor_pos, goal)
153
+ elif heuristic_type == "chebyshev":
154
+ h = self.chebyshev_distance(neighbor_pos, goal)
155
+ min_h = min(min_h, h)
156
+
157
+ # Create new node
158
+ new_node = Node(neighbor_pos, current_node, direction)
159
+ new_node.g = new_g
160
+ new_node.h = min_h
161
+ new_node.f = new_g + min_h
162
+
163
+ g_costs[neighbor_pos] = new_g
164
+ explored.add(neighbor_pos)
165
+ heapq.heappush(open_list, new_node)
166
+
167
+ return None, explored
168
+
169
+ def find_path_to_nearest_dirty(self, vacuum_pos, algorithm="a_star", heuristic="manhattan"):
170
+ """Find path to the nearest dirty cell"""
171
+ dirty_cells = list(self.env.dirty_cells)
172
+
173
+ if algorithm == "bfs":
174
+ return self.bfs(vacuum_pos, dirty_cells)
175
+ else:
176
+ # For A*, we need to decide whether to allow diagonals based on heuristic
177
+ allow_diagonals = heuristic in ["euclidean", "chebyshev"]
178
+ return self.a_star(vacuum_pos, dirty_cells, heuristic, allow_diagonals)
vacuum_simulation/V0/vacuum_simulation.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import time
3
+ from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
4
+ QPushButton, QComboBox, QCheckBox, QLabel, QWidget, QGridLayout,
5
+ QFrame)
6
+ from PyQt5.QtCore import QTimer, Qt
7
+ from PyQt5.QtGui import QColor, QPainter, QBrush, QFont, QPen
8
+
9
+ from environment import Environment, CellType
10
+ from search_algorithms import SearchAlgorithms
11
+
12
+ class GridWidget(QWidget):
13
+ def __init__(self, rows, cols, cell_size, environment):
14
+ super().__init__()
15
+ self.rows = rows
16
+ self.cols = cols
17
+ self.cell_size = cell_size
18
+ self.env = environment
19
+ self.current_path = []
20
+ self.current_direction = None
21
+
22
+ self.setFixedSize(cols * cell_size, rows * cell_size)
23
+
24
+ def paintEvent(self, event):
25
+ painter = QPainter(self)
26
+ painter.setRenderHint(QPainter.Antialiasing)
27
+
28
+ # Draw grid cells
29
+ for row in range(self.rows):
30
+ for col in range(self.cols):
31
+ x = col * self.cell_size
32
+ y = row * self.cell_size
33
+
34
+ # Determine cell color based on type
35
+ cell_type = self.env.grid[row][col]
36
+ if cell_type == CellType.CLEAN:
37
+ color = QColor(255, 255, 0) # Yellow
38
+ elif cell_type == CellType.DIRTY:
39
+ color = QColor(255, 0, 0) # Red
40
+ elif cell_type == CellType.OBSTACLE:
41
+ color = QColor(0, 0, 255) # Blue
42
+ elif cell_type == CellType.EXPLORED:
43
+ color = QColor(0, 255, 0) # Green
44
+
45
+ # Draw cell
46
+ painter.fillRect(x, y, self.cell_size, self.cell_size, color)
47
+ painter.setPen(Qt.black)
48
+ painter.drawRect(x, y, self.cell_size, self.cell_size)
49
+
50
+ # Draw path
51
+ if self.current_path:
52
+ painter.setPen(QPen(QColor(255, 165, 0), 2)) # Orange
53
+ painter.setBrush(QBrush(QColor(255, 165, 0)))
54
+
55
+ for i in range(len(self.current_path) - 1):
56
+ row1, col1 = self.current_path[i]
57
+ row2, col2 = self.current_path[i + 1]
58
+
59
+ x1 = col1 * self.cell_size + self.cell_size // 2
60
+ y1 = row1 * self.cell_size + self.cell_size // 2
61
+ x2 = col2 * self.cell_size + self.cell_size // 2
62
+ y2 = row2 * self.cell_size + self.cell_size // 2
63
+
64
+ painter.drawLine(x1, y1, x2, y2)
65
+
66
+ # Draw path nodes
67
+ for row, col in self.current_path:
68
+ x = col * self.cell_size + self.cell_size // 2
69
+ y = row * self.cell_size + self.cell_size // 2
70
+ painter.drawEllipse(x - 3, y - 3, 6, 6)
71
+
72
+ # Draw vacuum
73
+ if self.env.vacuum_pos:
74
+ row, col = self.env.vacuum_pos
75
+ x = col * self.cell_size
76
+ y = row * self.cell_size
77
+
78
+ painter.setPen(Qt.black)
79
+ painter.setBrush(QBrush(Qt.white))
80
+ painter.drawEllipse(x + 5, y + 5, self.cell_size - 10, self.cell_size - 10)
81
+
82
+ # Draw direction indicator
83
+ if self.current_direction is not None:
84
+ center_x = x + self.cell_size // 2
85
+ center_y = y + self.cell_size // 2
86
+
87
+ if self.current_direction.value == 0: # UP
88
+ painter.drawLine(center_x, center_y, center_x, y + 5)
89
+ elif self.current_direction.value == 1: # RIGHT
90
+ painter.drawLine(center_x, center_y, x + self.cell_size - 5, center_y)
91
+ elif self.current_direction.value == 2: # DOWN
92
+ painter.drawLine(center_x, center_y, center_x, y + self.cell_size - 5)
93
+ elif self.current_direction.value == 3: # LEFT
94
+ painter.drawLine(center_x, center_y, x + 5, center_y)
95
+
96
+ def update_path(self, path, direction):
97
+ self.current_path = path
98
+ self.current_direction = direction
99
+ self.update()
100
+
101
+ class VacuumSimulation(QMainWindow):
102
+ def __init__(self, rows=15, cols=15):
103
+ super().__init__()
104
+ self.rows = rows
105
+ self.cols = cols
106
+ self.cell_size = 30
107
+ self.env = Environment(rows, cols)
108
+ self.search = SearchAlgorithms(self.env)
109
+
110
+ # Simulation state
111
+ self.current_path = []
112
+ self.explored_cells = set()
113
+ self.steps_taken = 0
114
+ self.total_cost = 0
115
+ self.current_direction = None
116
+ self.is_running = False
117
+ self.timer = QTimer()
118
+ self.timer.timeout.connect(self.next_step)
119
+
120
+ self.init_ui()
121
+ self.update_display()
122
+
123
+ def init_ui(self):
124
+ self.setWindowTitle("Vacuum Cleaner Search Simulation")
125
+ self.setFixedSize(self.cols * self.cell_size + 300, self.rows * self.cell_size + 150)
126
+
127
+ # Central widget
128
+ central_widget = QWidget()
129
+ self.setCentralWidget(central_widget)
130
+
131
+ # Main layout
132
+ main_layout = QVBoxLayout()
133
+ central_widget.setLayout(main_layout)
134
+
135
+ # Metrics display
136
+ metrics_layout = QHBoxLayout()
137
+ self.steps_label = QLabel("Steps: 0")
138
+ self.cost_label = QLabel("Total Cost: 0.0")
139
+ self.dirty_label = QLabel("Dirty Cells: 0")
140
+ self.nodes_explored_label = QLabel("Nodes Explored: 0")
141
+
142
+ # Set fixed width for metrics to prevent layout shifting
143
+ self.steps_label.setFixedWidth(120)
144
+ self.cost_label.setFixedWidth(120)
145
+ self.dirty_label.setFixedWidth(120)
146
+ self.nodes_explored_label.setFixedWidth(140)
147
+
148
+ metrics_layout.addWidget(self.steps_label)
149
+ metrics_layout.addWidget(self.cost_label)
150
+ metrics_layout.addWidget(self.dirty_label)
151
+ metrics_layout.addWidget(self.nodes_explored_label)
152
+ metrics_layout.addStretch()
153
+
154
+ main_layout.addLayout(metrics_layout)
155
+
156
+ # Control panel
157
+ control_layout = QHBoxLayout()
158
+
159
+ # Reset button
160
+ self.reset_button = QPushButton("Reset")
161
+ self.reset_button.clicked.connect(self.reset_simulation)
162
+ control_layout.addWidget(self.reset_button)
163
+
164
+ # Next button
165
+ self.next_button = QPushButton("Next")
166
+ self.next_button.clicked.connect(self.next_step)
167
+ control_layout.addWidget(self.next_button)
168
+
169
+ # Run button
170
+ self.run_button = QPushButton("Run")
171
+ self.run_button.clicked.connect(self.toggle_run)
172
+ control_layout.addWidget(self.run_button)
173
+
174
+ # Turn cost checkbox
175
+ self.turn_cost_checkbox = QCheckBox("Turn Cost (0.5 per 90° turn)")
176
+ self.turn_cost_checkbox.stateChanged.connect(self.toggle_turn_cost)
177
+ control_layout.addWidget(self.turn_cost_checkbox)
178
+
179
+ # Search algorithm dropdown
180
+ control_layout.addWidget(QLabel("Search:"))
181
+ self.search_combo = QComboBox()
182
+ self.search_combo.addItems(["BFS", "A* Manhattan", "A* Euclidean", "A* Chebyshev"])
183
+ control_layout.addWidget(self.search_combo)
184
+
185
+ control_layout.addStretch()
186
+ main_layout.addLayout(control_layout)
187
+
188
+ # Grid display
189
+ self.grid_widget = GridWidget(self.rows, self.cols, self.cell_size, self.env)
190
+ main_layout.addWidget(self.grid_widget)
191
+
192
+ # Legend
193
+ legend_layout = QHBoxLayout()
194
+
195
+ def create_legend_item(color, text):
196
+ item_widget = QWidget()
197
+ item_layout = QHBoxLayout()
198
+ item_widget.setLayout(item_layout)
199
+
200
+ color_label = QLabel()
201
+ color_label.setFixedSize(20, 20)
202
+ color_label.setStyleSheet(f"background-color: {color}; border: 1px solid black")
203
+ item_layout.addWidget(color_label)
204
+
205
+ text_label = QLabel(text)
206
+ item_layout.addWidget(text_label)
207
+ item_layout.setContentsMargins(5, 0, 10, 0)
208
+
209
+ return item_widget
210
+
211
+ legend_layout.addWidget(create_legend_item("yellow", "Clean"))
212
+ legend_layout.addWidget(create_legend_item("red", "Dirty"))
213
+ legend_layout.addWidget(create_legend_item("blue", "Obstacle"))
214
+ legend_layout.addWidget(create_legend_item("green", "Explored"))
215
+ legend_layout.addWidget(create_legend_item("orange", "Path"))
216
+ legend_layout.addStretch()
217
+
218
+ main_layout.addLayout(legend_layout)
219
+
220
+ def update_display(self):
221
+ self.steps_label.setText(f"Steps: {self.steps_taken}")
222
+ self.cost_label.setText(f"Total Cost: {self.total_cost:.2f}")
223
+ self.dirty_label.setText(f"Dirty Cells: {self.env.get_dirty_count()}")
224
+ self.nodes_explored_label.setText(f"Nodes Explored: {len(self.explored_cells)}")
225
+ self.grid_widget.update_path(self.current_path, self.current_direction)
226
+
227
+ def reset_simulation(self):
228
+ self.env.reset()
229
+ self.current_path = []
230
+ self.explored_cells = set()
231
+ self.steps_taken = 0
232
+ self.total_cost = 0
233
+ self.current_direction = None
234
+ self.is_running = False
235
+ self.timer.stop()
236
+ self.run_button.setText("Run")
237
+ self.update_display()
238
+
239
+ def next_step(self):
240
+ if self.env.is_clean():
241
+ self.is_running = False
242
+ self.timer.stop()
243
+ self.run_button.setText("Run")
244
+ return
245
+
246
+ # If we don't have a path, find one
247
+ if not self.current_path:
248
+ self.find_path()
249
+
250
+ # If we have a path, move along it
251
+ if self.current_path:
252
+ # Move to next position in path
253
+ next_pos = self.current_path.pop(0)
254
+
255
+ # Calculate movement cost
256
+ move_cost = 1
257
+ if self.turn_cost_checkbox.isChecked() and self.current_direction is not None:
258
+ # Determine if we turned
259
+ current_row, current_col = self.env.vacuum_pos
260
+ next_row, next_col = next_pos
261
+
262
+ # Determine new direction
263
+ if next_row < current_row:
264
+ new_direction = 0 # UP
265
+ elif next_row > current_row:
266
+ new_direction = 2 # DOWN
267
+ elif next_col > current_col:
268
+ new_direction = 1 # RIGHT
269
+ else:
270
+ new_direction = 3 # LEFT
271
+
272
+ # Add turn cost if direction changed
273
+ if self.current_direction.value != new_direction:
274
+ move_cost += 0.5
275
+
276
+ self.current_direction = new_direction
277
+
278
+ # Update vacuum position
279
+ self.env.vacuum_pos = next_pos
280
+ self.steps_taken += 1
281
+ self.total_cost += move_cost
282
+
283
+ # Clean the cell if it's dirty
284
+ if self.env.clean_cell(next_pos[0], next_pos[1]):
285
+ # If we cleaned a cell, we need to find a new path
286
+ self.current_path = []
287
+
288
+ # Mark cell as explored
289
+ self.env.mark_explored(next_pos[0], next_pos[1])
290
+
291
+ self.update_display()
292
+
293
+ def find_path(self):
294
+ # Get selected search algorithm
295
+ search_type = self.search_combo.currentText()
296
+
297
+ if search_type == "BFS":
298
+ algorithm = "bfs"
299
+ heuristic = "manhattan"
300
+ else:
301
+ algorithm = "a_star"
302
+ if search_type == "A* Manhattan":
303
+ heuristic = "manhattan"
304
+ elif search_type == "A* Euclidean":
305
+ heuristic = "euclidean"
306
+ else: # A* Chebyshev
307
+ heuristic = "chebyshev"
308
+
309
+ # Update search algorithm with current turn cost setting
310
+ self.search.turn_cost_enabled = self.turn_cost_checkbox.isChecked()
311
+
312
+ # Find path to nearest dirty cell
313
+ self.current_path, explored = self.search.find_path_to_nearest_dirty(
314
+ self.env.vacuum_pos, algorithm, heuristic)
315
+
316
+ if self.current_path:
317
+ # Remove current position from path
318
+ self.current_path = self.current_path[1:]
319
+ self.explored_cells.update(explored)
320
+
321
+ # Mark explored cells
322
+ for row, col in explored:
323
+ if (row, col) != self.env.vacuum_pos and self.env.grid[row][col] == CellType.CLEAN:
324
+ self.env.grid[row][col] = CellType.EXPLORED
325
+
326
+ def toggle_run(self):
327
+ self.is_running = not self.is_running
328
+
329
+ if self.is_running:
330
+ self.run_button.setText("Pause")
331
+ self.timer.start(1000) # 1 second interval
332
+ else:
333
+ self.run_button.setText("Run")
334
+ self.timer.stop()
335
+
336
+ def toggle_turn_cost(self):
337
+ # When turn cost is toggled, we need to recalculate the path
338
+ if self.current_path:
339
+ self.current_path = []
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.23.03 PM.png ADDED

Git LFS Details

  • SHA256: 3933e1cbdf7264c136693f7e63c957858482dd7d66b292a71186834694d70ee7
  • Pointer size: 131 Bytes
  • Size of remote file: 249 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.24.27 PM.png ADDED

Git LFS Details

  • SHA256: 0d1abc728eb98e7ad64b0c3616660f21ed36428423b69c606124fd19cfd4fd13
  • Pointer size: 131 Bytes
  • Size of remote file: 252 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.24.52 PM.png ADDED

Git LFS Details

  • SHA256: 4cb65f961e3934ab4bc86331c46ec593e53eaf5f7c60442ff91f110797eb4ee2
  • Pointer size: 131 Bytes
  • Size of remote file: 259 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.26.11 PM.png ADDED

Git LFS Details

  • SHA256: 3122cec039b19784fe6ea2168190a2b9fce0fa98f08b06abdb037ecf4651686a
  • Pointer size: 131 Bytes
  • Size of remote file: 260 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.27.03 PM.png ADDED

Git LFS Details

  • SHA256: 62f14dbac73545026bc0b64af5207249fe0091723ef9f2b03ba13fcbb2550c75
  • Pointer size: 131 Bytes
  • Size of remote file: 265 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.28.01 PM.png ADDED

Git LFS Details

  • SHA256: f70b75b1a1c0ad98a9066497d2b60bd5316a2983f7fc69883f44213d4c3302e5
  • Pointer size: 131 Bytes
  • Size of remote file: 268 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.28.29 PM.png ADDED

Git LFS Details

  • SHA256: 6d5394696c5f1d6e41719ae0e839e117183622ec1191f3d9067962188bc2d71f
  • Pointer size: 131 Bytes
  • Size of remote file: 267 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.29.33 PM.png ADDED

Git LFS Details

  • SHA256: 3454d6d53e0504a3e82404050a1f89af25f719372734154731efa63f928a3ebc
  • Pointer size: 131 Bytes
  • Size of remote file: 284 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.30.06 PM.png ADDED

Git LFS Details

  • SHA256: e5b5ca51a0235ad61e5bd413fd0081feb5d01022be5582a7cd2f3dffac050ab8
  • Pointer size: 131 Bytes
  • Size of remote file: 291 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.31.17 PM.png ADDED

Git LFS Details

  • SHA256: da1c0f72a81a6b56f377af23a9e093eab128a4d3815ae35d529926c9c883b5a6
  • Pointer size: 131 Bytes
  • Size of remote file: 289 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.31.42 PM.png ADDED

Git LFS Details

  • SHA256: 6a35946eea477a6414e69b9b842f8668a2ef7f0faaa731f39a1f5944d4a4cb34
  • Pointer size: 131 Bytes
  • Size of remote file: 297 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.33.19 PM.png ADDED

Git LFS Details

  • SHA256: 1820f45b26873db5ab25677ae311b4d694acd6ad231d7896fe12c4f2f862a03c
  • Pointer size: 131 Bytes
  • Size of remote file: 291 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.33.42 PM.png ADDED

Git LFS Details

  • SHA256: 1b7b2ddbcd839b7ebb79115f92bafccbab59f74ab0bceab9dedbeca66b4c539d
  • Pointer size: 131 Bytes
  • Size of remote file: 296 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.34.51 PM.png ADDED

Git LFS Details

  • SHA256: 088211b1aade7692434b71f703fe00b6c550e1bcf98e160e1192b8896c803748
  • Pointer size: 131 Bytes
  • Size of remote file: 291 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.35.56 PM.png ADDED

Git LFS Details

  • SHA256: c8ab342256b9c7aba9a641e953a257af7856bd431f9a067c3019433a4d31b2a7
  • Pointer size: 131 Bytes
  • Size of remote file: 301 kB
vacuum_simulation/V1/V1 Visualizations/Screenshot 2025-10-28 at 12.36.55 PM.png ADDED

Git LFS Details

  • SHA256: d6ffd992ca38ba0c3df5867743fca87a5ea4e8fa9fdd8d589a7d478edaf2f40a
  • Pointer size: 131 Bytes
  • Size of remote file: 295 kB
vacuum_simulation/__pycache__/environment.cpython-313.pyc ADDED
Binary file (5.89 kB). View file
 
vacuum_simulation/__pycache__/search_algorithms.cpython-313.pyc ADDED
Binary file (9.73 kB). View file
 
vacuum_simulation/__pycache__/vacuum_simulation.cpython-313.pyc ADDED
Binary file (25.4 kB). View file
 
vacuum_simulation/environment.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from enum import Enum
3
+
4
+ class CellType(Enum):
5
+ CLEAN = 0
6
+ DIRTY = 1
7
+ OBSTACLE = 2
8
+ EXPLORED = 3
9
+
10
+ class Direction(Enum):
11
+ UP = 0
12
+ RIGHT = 1
13
+ DOWN = 2
14
+ LEFT = 3
15
+
16
+ @classmethod
17
+ def from_movement(cls, current_pos, next_pos):
18
+ """Determine direction from current position to next position"""
19
+ current_row, current_col = current_pos
20
+ next_row, next_col = next_pos
21
+
22
+ if next_row < current_row:
23
+ return cls.UP
24
+ elif next_row > current_row:
25
+ return cls.DOWN
26
+ elif next_col > current_col:
27
+ return cls.RIGHT
28
+ elif next_col < current_col:
29
+ return cls.LEFT
30
+ return None
31
+
32
+ class Environment:
33
+ def __init__(self, rows, cols, obstacle_density=0.2, dirt_density=0.3):
34
+ self.rows = rows
35
+ self.cols = cols
36
+ self.grid = [[CellType.CLEAN for _ in range(cols)] for _ in range(rows)]
37
+ self.obstacle_density = obstacle_density
38
+ self.dirt_density = dirt_density
39
+ self.vacuum_pos = None
40
+ self.dirty_cells = set()
41
+
42
+ self.generate_environment()
43
+
44
+ def generate_environment(self):
45
+ # Place obstacles
46
+ for i in range(self.rows):
47
+ for j in range(self.cols):
48
+ if random.random() < self.obstacle_density:
49
+ self.grid[i][j] = CellType.OBSTACLE
50
+
51
+ # Place dirt on clean cells only
52
+ for i in range(self.rows):
53
+ for j in range(self.cols):
54
+ if self.grid[i][j] == CellType.CLEAN and random.random() < self.dirt_density:
55
+ self.grid[i][j] = CellType.DIRTY
56
+ self.dirty_cells.add((i, j))
57
+
58
+ # Place vacuum at a random clean position
59
+ clean_positions = [(i, j) for i in range(self.rows) for j in range(self.cols)
60
+ if self.grid[i][j] == CellType.CLEAN]
61
+ if clean_positions:
62
+ self.vacuum_pos = random.choice(clean_positions)
63
+
64
+ def reset(self):
65
+ self.grid = [[CellType.CLEAN for _ in range(self.cols)] for _ in range(self.rows)]
66
+ self.dirty_cells = set()
67
+ self.generate_environment()
68
+
69
+ def is_valid_position(self, row, col):
70
+ return (0 <= row < self.rows and
71
+ 0 <= col < self.cols and
72
+ self.grid[row][col] != CellType.OBSTACLE)
73
+
74
+ def clean_cell(self, row, col):
75
+ if (row, col) in self.dirty_cells:
76
+ self.grid[row][col] = CellType.CLEAN
77
+ self.dirty_cells.remove((row, col))
78
+ return True
79
+ return False
80
+
81
+ def mark_explored(self, row, col):
82
+ if self.grid[row][col] == CellType.CLEAN:
83
+ self.grid[row][col] = CellType.EXPLORED
84
+
85
+ def get_dirty_count(self):
86
+ return len(self.dirty_cells)
87
+
88
+ def is_clean(self):
89
+ return len(self.dirty_cells) == 0
vacuum_simulation/main.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import random
3
+ from PyQt5.QtWidgets import QApplication
4
+ from vacuum_simulation import VacuumSimulation
5
+
6
+ if __name__ == "__main__":
7
+ app = QApplication(sys.argv)
8
+
9
+ # Default grid size or use command line arguments
10
+ rows, cols = 15, 15
11
+ if len(sys.argv) >= 3:
12
+ try:
13
+ rows, cols = int(sys.argv[1]), int(sys.argv[2])
14
+ except ValueError:
15
+ print("Invalid grid dimensions. Using default 15x15.")
16
+
17
+ window = VacuumSimulation(rows, cols)
18
+ window.show()
19
+
20
+ sys.exit(app.exec_())
vacuum_simulation/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ PyQt5
vacuum_simulation/search_algorithms.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import heapq
2
+ import math
3
+ import time
4
+ from collections import deque
5
+ from environment import Direction
6
+
7
+ class Node:
8
+ def __init__(self, position, parent=None, direction=None):
9
+ self.position = position
10
+ self.parent = parent
11
+ self.direction = direction # This should be a Direction enum
12
+ self.g = 0 # Cost from start to current node
13
+ self.h = 0 # Heuristic cost from current node to goal
14
+ self.f = 0 # Total cost (g + h)
15
+
16
+ def __eq__(self, other):
17
+ return self.position == other.position
18
+
19
+ def __lt__(self, other):
20
+ return self.f < other.f
21
+
22
+ class SearchAlgorithms:
23
+ def __init__(self, environment, turn_cost_enabled=False):
24
+ self.env = environment
25
+ self.turn_cost_enabled = turn_cost_enabled
26
+
27
+ def calculate_turn_cost(self, current_dir, new_dir):
28
+ """Calculate turn cost between directions (0.5 for 90° turns)"""
29
+ if not self.turn_cost_enabled or current_dir is None or new_dir is None:
30
+ return 0
31
+
32
+ if current_dir == new_dir:
33
+ return 0
34
+
35
+ # Calculate the absolute difference in direction values
36
+ diff = abs(current_dir.value - new_dir.value)
37
+
38
+ # For 4-direction system, handle wrap-around (UP=0, LEFT=3)
39
+ if diff == 3: # This means UP to LEFT or LEFT to UP
40
+ diff = 1
41
+
42
+ if diff == 1: # 90° turn
43
+ return 0.5
44
+ elif diff == 2: # 180° turn
45
+ return 1.0
46
+ return 0
47
+
48
+ def get_neighbors(self, position, direction=None):
49
+ row, col = position
50
+ neighbors = []
51
+
52
+ # Possible moves: up, right, down, left
53
+ moves = [
54
+ (-1, 0, Direction.UP),
55
+ (0, 1, Direction.RIGHT),
56
+ (1, 0, Direction.DOWN),
57
+ (0, -1, Direction.LEFT)
58
+ ]
59
+
60
+ for dr, dc, new_dir in moves:
61
+ new_row, new_col = row + dr, col + dc
62
+ if self.env.is_valid_position(new_row, new_col):
63
+ turn_cost = self.calculate_turn_cost(direction, new_dir)
64
+ move_cost = 1 + turn_cost
65
+ neighbors.append(((new_row, new_col), new_dir, move_cost))
66
+
67
+ return neighbors
68
+
69
+ def get_diagonal_neighbors(self, position, direction=None):
70
+ row, col = position
71
+ neighbors = []
72
+
73
+ # Possible moves including diagonals
74
+ moves = [
75
+ (-1, 0, Direction.UP, 1),
76
+ (0, 1, Direction.RIGHT, 1),
77
+ (1, 0, Direction.DOWN, 1),
78
+ (0, -1, Direction.LEFT, 1),
79
+ (-1, -1, Direction.UP, math.sqrt(2)),
80
+ (-1, 1, Direction.UP, math.sqrt(2)),
81
+ (1, -1, Direction.DOWN, math.sqrt(2)),
82
+ (1, 1, Direction.DOWN, math.sqrt(2))
83
+ ]
84
+
85
+ for dr, dc, new_dir, base_cost in moves:
86
+ new_row, new_col = row + dr, col + dc
87
+ if self.env.is_valid_position(new_row, new_col):
88
+ turn_cost = self.calculate_turn_cost(direction, new_dir)
89
+ move_cost = base_cost + turn_cost
90
+ neighbors.append(((new_row, new_col), new_dir, move_cost))
91
+
92
+ return neighbors
93
+
94
+ def manhattan_distance(self, pos1, pos2):
95
+ return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1])
96
+
97
+ def euclidean_distance(self, pos1, pos2):
98
+ return math.sqrt((pos1[0] - pos2[0])**2 + (pos1[1] - pos2[1])**2)
99
+
100
+ def chebyshev_distance(self, pos1, pos2):
101
+ return max(abs(pos1[0] - pos2[0]), abs(pos1[1] - pos2[1]))
102
+
103
+ def bfs(self, start, goals):
104
+ """Breadth-First Search"""
105
+ start_time = time.time()
106
+ if not goals:
107
+ return None, set(), 0, 0
108
+
109
+ queue = deque([Node(start)])
110
+ visited = set([start])
111
+ explored = set([start])
112
+ nodes_expanded = 0
113
+
114
+ while queue:
115
+ current_node = queue.popleft()
116
+ nodes_expanded += 1
117
+
118
+ # Check if we reached any goal
119
+ if current_node.position in goals:
120
+ path = []
121
+ temp_node = current_node
122
+ while temp_node:
123
+ path.append(temp_node.position)
124
+ temp_node = temp_node.parent
125
+ computation_time = time.time() - start_time
126
+ return path[::-1], explored, nodes_expanded, computation_time
127
+
128
+ for neighbor_pos, new_dir, move_cost in self.get_neighbors(current_node.position):
129
+ if neighbor_pos not in visited:
130
+ visited.add(neighbor_pos)
131
+ explored.add(neighbor_pos)
132
+ new_node = Node(neighbor_pos, current_node, new_dir)
133
+ queue.append(new_node)
134
+
135
+ computation_time = time.time() - start_time
136
+ return None, explored, nodes_expanded, computation_time
137
+
138
+ def a_star(self, start, goals, heuristic_type="manhattan", allow_diagonals=False):
139
+ """A* Search with different heuristics"""
140
+ start_time = time.time()
141
+ if not goals:
142
+ return None, set(), 0, 0
143
+
144
+ open_list = []
145
+ start_node = Node(start)
146
+ heapq.heappush(open_list, start_node)
147
+ closed_list = set()
148
+ explored = set([start])
149
+ nodes_expanded = 0
150
+
151
+ # Cost from start to node
152
+ g_costs = {start: 0}
153
+ # Keep track of directions for turn cost calculation
154
+ directions = {start: None}
155
+
156
+ while open_list:
157
+ current_node = heapq.heappop(open_list)
158
+ nodes_expanded += 1
159
+
160
+ # Check if we reached any goal
161
+ if current_node.position in goals:
162
+ path = []
163
+ temp_node = current_node
164
+ while temp_node:
165
+ path.append(temp_node.position)
166
+ temp_node = temp_node.parent
167
+ computation_time = time.time() - start_time
168
+ return path[::-1], explored, nodes_expanded, computation_time
169
+
170
+ closed_list.add(current_node.position)
171
+ current_dir = directions[current_node.position]
172
+
173
+ # Get neighbors based on whether diagonals are allowed
174
+ if allow_diagonals:
175
+ neighbors = self.get_diagonal_neighbors(current_node.position, current_dir)
176
+ else:
177
+ neighbors = self.get_neighbors(current_node.position, current_dir)
178
+
179
+ for neighbor_pos, direction, move_cost in neighbors:
180
+ if neighbor_pos in closed_list:
181
+ continue
182
+
183
+ # Calculate new g cost
184
+ new_g = g_costs[current_node.position] + move_cost
185
+
186
+ if neighbor_pos not in g_costs or new_g < g_costs[neighbor_pos]:
187
+ # Calculate heuristic to the closest goal
188
+ min_h = float('inf')
189
+ for goal in goals:
190
+ if heuristic_type == "manhattan":
191
+ h = self.manhattan_distance(neighbor_pos, goal)
192
+ elif heuristic_type == "euclidean":
193
+ h = self.euclidean_distance(neighbor_pos, goal)
194
+ elif heuristic_type == "chebyshev":
195
+ h = self.chebyshev_distance(neighbor_pos, goal)
196
+ min_h = min(min_h, h)
197
+
198
+ # Create new node
199
+ new_node = Node(neighbor_pos, current_node, direction)
200
+ new_node.g = new_g
201
+ new_node.h = min_h
202
+ new_node.f = new_g + min_h
203
+
204
+ g_costs[neighbor_pos] = new_g
205
+ directions[neighbor_pos] = direction
206
+ explored.add(neighbor_pos)
207
+ heapq.heappush(open_list, new_node)
208
+
209
+ computation_time = time.time() - start_time
210
+ return None, explored, nodes_expanded, computation_time
211
+
212
+ def find_path_to_nearest_dirty(self, vacuum_pos, algorithm="a_star", heuristic="manhattan"):
213
+ """Find path to the nearest dirty cell"""
214
+ dirty_cells = list(self.env.dirty_cells)
215
+
216
+ if not dirty_cells:
217
+ return None, set(), 0, 0
218
+
219
+ if algorithm == "bfs":
220
+ return self.bfs(vacuum_pos, dirty_cells)
221
+ else:
222
+ # For A*, we need to decide whether to allow diagonals based on heuristic
223
+ allow_diagonals = heuristic in ["euclidean", "chebyshev"]
224
+ return self.a_star(vacuum_pos, dirty_cells, heuristic, allow_diagonals)
vacuum_simulation/vacuum_simulation.py ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import time
3
+ from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
4
+ QPushButton, QComboBox, QCheckBox, QLabel, QWidget,
5
+ QTextEdit, QSplitter, QFrame)
6
+ from PyQt5.QtCore import QTimer, Qt
7
+ from PyQt5.QtGui import QColor, QPainter, QBrush, QPen
8
+
9
+ from environment import Environment, CellType, Direction
10
+ from search_algorithms import SearchAlgorithms
11
+
12
+ class GridWidget(QWidget):
13
+ def __init__(self, rows, cols, cell_size, environment):
14
+ super().__init__()
15
+ self.rows = rows
16
+ self.cols = cols
17
+ self.cell_size = cell_size
18
+ self.env = environment
19
+ self.current_path = []
20
+ self.current_direction = None
21
+
22
+ self.setFixedSize(cols * cell_size, rows * cell_size)
23
+
24
+ def paintEvent(self, event):
25
+ painter = QPainter(self)
26
+ painter.setRenderHint(QPainter.Antialiasing)
27
+
28
+ # Draw grid cells
29
+ for row in range(self.rows):
30
+ for col in range(self.cols):
31
+ x = col * self.cell_size
32
+ y = row * self.cell_size
33
+
34
+ # Determine cell color based on type
35
+ cell_type = self.env.grid[row][col]
36
+ if cell_type == CellType.CLEAN:
37
+ color = QColor(255, 255, 0) # Yellow
38
+ elif cell_type == CellType.DIRTY:
39
+ color = QColor(255, 0, 0) # Red
40
+ elif cell_type == CellType.OBSTACLE:
41
+ color = QColor(0, 0, 255) # Blue
42
+ elif cell_type == CellType.EXPLORED:
43
+ color = QColor(0, 255, 0) # Green
44
+
45
+ # Draw cell
46
+ painter.fillRect(x, y, self.cell_size, self.cell_size, color)
47
+ painter.setPen(Qt.black)
48
+ painter.drawRect(x, y, self.cell_size, self.cell_size)
49
+
50
+ # Draw path
51
+ if self.current_path:
52
+ painter.setPen(QPen(QColor(255, 165, 0), 3)) # Orange, thicker line
53
+ painter.setBrush(QBrush(QColor(255, 165, 0)))
54
+
55
+ # Draw path lines
56
+ for i in range(len(self.current_path) - 1):
57
+ row1, col1 = self.current_path[i]
58
+ row2, col2 = self.current_path[i + 1]
59
+
60
+ x1 = col1 * self.cell_size + self.cell_size // 2
61
+ y1 = row1 * self.cell_size + self.cell_size // 2
62
+ x2 = col2 * self.cell_size + self.cell_size // 2
63
+ y2 = row2 * self.cell_size + self.cell_size // 2
64
+
65
+ painter.drawLine(x1, y1, x2, y2)
66
+
67
+ # Draw path nodes (larger dots)
68
+ for i, (row, col) in enumerate(self.current_path):
69
+ x = col * self.cell_size + self.cell_size // 2
70
+ y = row * self.cell_size + self.cell_size // 2
71
+
72
+ # Make start and end points different
73
+ if i == 0: # Start
74
+ painter.setBrush(QBrush(QColor(0, 255, 0))) # Green
75
+ radius = 6
76
+ elif i == len(self.current_path) - 1: # End
77
+ painter.setBrush(QBrush(QColor(255, 0, 0))) # Red
78
+ radius = 6
79
+ else: # Intermediate
80
+ painter.setBrush(QBrush(QColor(255, 165, 0))) # Orange
81
+ radius = 4
82
+
83
+ painter.drawEllipse(x - radius, y - radius, radius * 2, radius * 2)
84
+
85
+ # Draw vacuum
86
+ if self.env.vacuum_pos:
87
+ row, col = self.env.vacuum_pos
88
+ x = col * self.cell_size
89
+ y = row * self.cell_size
90
+
91
+ # Draw vacuum as a circle with direction indicator
92
+ painter.setPen(QPen(Qt.black, 2))
93
+ painter.setBrush(QBrush(Qt.white))
94
+ painter.drawEllipse(x + 5, y + 5, self.cell_size - 10, self.cell_size - 10)
95
+
96
+ # Draw direction indicator
97
+ if self.current_direction is not None:
98
+ center_x = x + self.cell_size // 2
99
+ center_y = y + self.cell_size // 2
100
+
101
+ # Thicker direction indicator
102
+ direction_pen = QPen(Qt.black, 3)
103
+ painter.setPen(direction_pen)
104
+
105
+ # Use the Direction enum properly
106
+ if self.current_direction == Direction.UP:
107
+ painter.drawLine(center_x, center_y + 5, center_x, y + 5)
108
+ elif self.current_direction == Direction.RIGHT:
109
+ painter.drawLine(center_x - 5, center_y, x + self.cell_size - 5, center_y)
110
+ elif self.current_direction == Direction.DOWN:
111
+ painter.drawLine(center_x, center_y - 5, center_x, y + self.cell_size - 5)
112
+ elif self.current_direction == Direction.LEFT:
113
+ painter.drawLine(center_x + 5, center_y, x + 5, center_y)
114
+
115
+ def update_path(self, path, direction):
116
+ self.current_path = path
117
+ self.current_direction = direction
118
+ self.update()
119
+
120
+ class VacuumSimulation(QMainWindow):
121
+ def __init__(self, rows=15, cols=15):
122
+ super().__init__()
123
+ self.rows = rows
124
+ self.cols = cols
125
+ self.cell_size = 30
126
+ self.env = Environment(rows, cols)
127
+ self.search = SearchAlgorithms(self.env)
128
+
129
+ # Simulation state
130
+ self.current_path = []
131
+ self.explored_cells = set()
132
+ self.steps_taken = 0
133
+ self.total_cost = 0
134
+ self.current_direction = None
135
+ self.is_running = False
136
+ self.timer = QTimer()
137
+ self.timer.timeout.connect(self.next_step)
138
+
139
+ # Performance metrics
140
+ self.total_nodes_expanded = 0
141
+ self.total_computation_time = 0
142
+ self.algorithm_stats = {
143
+ "BFS": {"runs": 0, "total_nodes": 0, "total_time": 0},
144
+ "A* Manhattan": {"runs": 0, "total_nodes": 0, "total_time": 0},
145
+ "A* Euclidean": {"runs": 0, "total_nodes": 0, "total_time": 0},
146
+ "A* Chebyshev": {"runs": 0, "total_nodes": 0, "total_time": 0}
147
+ }
148
+
149
+ self.init_ui()
150
+ self.update_display()
151
+
152
+ def init_ui(self):
153
+ self.setWindowTitle("Vacuum Cleaner Search Simulation - Algorithm Comparison")
154
+
155
+ # Use a larger window to accommodate side panel
156
+ grid_width = self.cols * self.cell_size
157
+ grid_height = self.rows * self.cell_size
158
+ self.setMinimumSize(grid_width + 400, grid_height + 200)
159
+
160
+ # Central widget
161
+ central_widget = QWidget()
162
+ self.setCentralWidget(central_widget)
163
+
164
+ # Main layout using splitter for resizable panels
165
+ main_layout = QHBoxLayout()
166
+ central_widget.setLayout(main_layout)
167
+
168
+ # Left side: Grid and controls
169
+ left_widget = QWidget()
170
+ left_layout = QVBoxLayout()
171
+ left_widget.setLayout(left_layout)
172
+
173
+ # Right side: Information panel
174
+ right_widget = QWidget()
175
+ right_widget.setMaximumWidth(350)
176
+ right_layout = QVBoxLayout()
177
+ right_widget.setLayout(right_layout)
178
+
179
+ # === LEFT PANEL: Grid and Controls ===
180
+
181
+ # Metrics display at top
182
+ metrics_layout = QHBoxLayout()
183
+
184
+ # Left metrics column
185
+ left_metrics = QVBoxLayout()
186
+ self.steps_label = QLabel("Steps: 0")
187
+ self.cost_label = QLabel("Total Cost: 0.0")
188
+ self.turn_cost_label = QLabel("Turn Cost: 0.0")
189
+ self.dirty_label = QLabel("Dirty Cells: 0")
190
+
191
+ left_metrics.addWidget(self.steps_label)
192
+ left_metrics.addWidget(self.cost_label)
193
+ left_metrics.addWidget(self.turn_cost_label)
194
+ left_metrics.addWidget(self.dirty_label)
195
+
196
+ # Right metrics column
197
+ right_metrics = QVBoxLayout()
198
+ self.nodes_explored_label = QLabel("Nodes Explored: 0")
199
+ self.nodes_expanded_label = QLabel("Nodes Expanded: 0")
200
+ self.comp_time_label = QLabel("Comp Time: 0.000s")
201
+ self.algorithm_label = QLabel("Algorithm: None")
202
+
203
+ right_metrics.addWidget(self.nodes_explored_label)
204
+ right_metrics.addWidget(self.nodes_expanded_label)
205
+ right_metrics.addWidget(self.comp_time_label)
206
+ right_metrics.addWidget(self.algorithm_label)
207
+
208
+ metrics_layout.addLayout(left_metrics)
209
+ metrics_layout.addLayout(right_metrics)
210
+ metrics_layout.addStretch()
211
+
212
+ left_layout.addLayout(metrics_layout)
213
+
214
+ # Control panel
215
+ control_layout = QHBoxLayout()
216
+
217
+ # Reset button
218
+ self.reset_button = QPushButton("Reset")
219
+ self.reset_button.clicked.connect(self.reset_simulation)
220
+ control_layout.addWidget(self.reset_button)
221
+
222
+ # Next button
223
+ self.next_button = QPushButton("Next")
224
+ self.next_button.clicked.connect(self.next_step)
225
+ control_layout.addWidget(self.next_button)
226
+
227
+ # Run button
228
+ self.run_button = QPushButton("Run")
229
+ self.run_button.clicked.connect(self.toggle_run)
230
+ control_layout.addWidget(self.run_button)
231
+
232
+ control_layout.addStretch()
233
+ left_layout.addLayout(control_layout)
234
+
235
+ # Search options
236
+ search_layout = QHBoxLayout()
237
+
238
+ # Turn cost checkbox
239
+ self.turn_cost_checkbox = QCheckBox("Turn Cost (0.5 per 90° turn)")
240
+ self.turn_cost_checkbox.stateChanged.connect(self.toggle_turn_cost)
241
+ search_layout.addWidget(self.turn_cost_checkbox)
242
+
243
+ # Search algorithm dropdown
244
+ search_layout.addWidget(QLabel("Search:"))
245
+ self.search_combo = QComboBox()
246
+ self.search_combo.addItems(["BFS", "A* Manhattan", "A* Euclidean", "A* Chebyshev"])
247
+ search_layout.addWidget(self.search_combo)
248
+
249
+ search_layout.addStretch()
250
+ left_layout.addLayout(search_layout)
251
+
252
+ # Grid display
253
+ self.grid_widget = GridWidget(self.rows, self.cols, self.cell_size, self.env)
254
+ left_layout.addWidget(self.grid_widget)
255
+
256
+ # Legend (moved to bottom of left panel)
257
+ legend_layout = QHBoxLayout()
258
+
259
+ def create_legend_item(color, text):
260
+ item_widget = QWidget()
261
+ item_layout = QHBoxLayout()
262
+ item_widget.setLayout(item_layout)
263
+
264
+ color_label = QLabel()
265
+ color_label.setFixedSize(16, 16)
266
+ color_label.setStyleSheet(f"background-color: {color}; border: 1px solid black")
267
+ item_layout.addWidget(color_label)
268
+
269
+ text_label = QLabel(text)
270
+ text_label.setStyleSheet("font-size: 10px;")
271
+ item_layout.addWidget(text_label)
272
+ item_layout.setContentsMargins(2, 0, 5, 0)
273
+
274
+ return item_widget
275
+
276
+ legend_layout.addWidget(create_legend_item("yellow", "Clean"))
277
+ legend_layout.addWidget(create_legend_item("red", "Dirty"))
278
+ legend_layout.addWidget(create_legend_item("blue", "Obstacle"))
279
+ legend_layout.addWidget(create_legend_item("green", "Explored"))
280
+ legend_layout.addWidget(create_legend_item("orange", "Path"))
281
+ legend_layout.addStretch()
282
+
283
+ left_layout.addLayout(legend_layout)
284
+
285
+ # === RIGHT PANEL: Information and Statistics ===
286
+
287
+ # Algorithm Information
288
+ info_label = QLabel("Algorithm Information:")
289
+ info_label.setStyleSheet("font-weight: bold; font-size: 12px; margin-bottom: 5px;")
290
+ right_layout.addWidget(info_label)
291
+
292
+ info_text = QTextEdit()
293
+ info_text.setMaximumHeight(120)
294
+ info_text.setReadOnly(True)
295
+ info_text.setHtml("""
296
+ <b>Search Algorithms:</b><br>
297
+ • <b>BFS</b>: Explores all directions equally, finds shortest path<br>
298
+ • <b>A* Manhattan</b>: Uses city-block distance heuristic<br>
299
+ • <b>A* Euclidean</b>: Uses straight-line distance heuristic<br>
300
+ • <b>A* Chebyshev</b>: Uses chessboard distance heuristic<br><br>
301
+ <b>Turn Cost</b>: When enabled, 90° turns cost +0.5
302
+ """)
303
+ right_layout.addWidget(info_text)
304
+
305
+ # Performance Statistics
306
+ stats_label = QLabel("Algorithm Performance:")
307
+ stats_label.setStyleSheet("font-weight: bold; font-size: 12px; margin-top: 10px; margin-bottom: 5px;")
308
+ right_layout.addWidget(stats_label)
309
+
310
+ self.stats_text = QTextEdit()
311
+ self.stats_text.setReadOnly(True)
312
+ self.stats_text.setStyleSheet("font-family: monospace; font-size: 10px;")
313
+ right_layout.addWidget(self.stats_text)
314
+
315
+ # Current Run Analysis
316
+ analysis_label = QLabel("Current Run Analysis:")
317
+ analysis_label.setStyleSheet("font-weight: bold; font-size: 12px; margin-top: 10px; margin-bottom: 5px;")
318
+ right_layout.addWidget(analysis_label)
319
+
320
+ self.analysis_text = QTextEdit()
321
+ self.analysis_text.setMaximumHeight(100)
322
+ self.analysis_text.setReadOnly(True)
323
+ self.analysis_text.setStyleSheet("font-family: monospace; font-size: 10px;")
324
+ right_layout.addWidget(self.analysis_text)
325
+
326
+ # Add stretch to push everything to top
327
+ right_layout.addStretch()
328
+
329
+ # Add both panels to main layout
330
+ main_layout.addWidget(left_widget)
331
+ main_layout.addWidget(right_widget)
332
+
333
+ # Set left widget to expand, right widget fixed width
334
+ main_layout.setStretchFactor(left_widget, 1)
335
+ main_layout.setStretchFactor(right_widget, 0)
336
+
337
+ self.update_stats_display()
338
+
339
+ def update_stats_display(self):
340
+ stats_text = "<pre>"
341
+ for algo, stats in self.algorithm_stats.items():
342
+ if stats["runs"] > 0:
343
+ avg_nodes = stats["total_nodes"] / stats["runs"]
344
+ avg_time = stats["total_time"] / stats["runs"]
345
+ stats_text += f"{algo:<15} {stats['runs']:>3} runs\n"
346
+ stats_text += f" Avg Nodes: {avg_nodes:>6.1f}\n"
347
+ stats_text += f" Avg Time: {avg_time:>7.4f}s\n"
348
+ else:
349
+ stats_text += f"{algo:<15} No runs yet\n"
350
+ stats_text += "</pre>"
351
+
352
+ # Add Chebyshev vs Euclidean comparison to analysis
353
+ chebyshev_stats = self.algorithm_stats["A* Chebyshev"]
354
+ euclidean_stats = self.algorithm_stats["A* Euclidean"]
355
+
356
+ analysis_text = "<pre>"
357
+ if chebyshev_stats["runs"] > 0 and euclidean_stats["runs"] > 0:
358
+ chebyshev_avg_nodes = chebyshev_stats["total_nodes"] / chebyshev_stats["runs"]
359
+ euclidean_avg_nodes = euclidean_stats["total_nodes"] / euclidean_stats["runs"]
360
+ chebyshev_avg_time = chebyshev_stats["total_time"] / chebyshev_stats["runs"]
361
+ euclidean_avg_time = euclidean_stats["total_time"] / euclidean_stats["runs"]
362
+
363
+ if euclidean_avg_nodes > 0:
364
+ node_ratio = chebyshev_avg_nodes / euclidean_avg_nodes
365
+ analysis_text += f"Node Exploration:\n"
366
+ analysis_text += f" Chebyshev explores {node_ratio:.1f}x\n"
367
+ analysis_text += f" more nodes than Euclidean\n\n"
368
+
369
+ if euclidean_avg_time > 0:
370
+ time_ratio = chebyshev_avg_time / euclidean_avg_time
371
+ analysis_text += f"Computation Time:\n"
372
+ analysis_text += f" Chebyshev is {time_ratio:.1f}x\n"
373
+ analysis_text += f" slower than Euclidean"
374
+ else:
375
+ analysis_text += "Run simulations to see\nalgorithm comparisons"
376
+ analysis_text += "</pre>"
377
+
378
+ self.stats_text.setHtml(stats_text)
379
+ self.analysis_text.setHtml(analysis_text)
380
+
381
+ def update_display(self):
382
+ self.steps_label.setText(f"Steps: {self.steps_taken}")
383
+ self.cost_label.setText(f"Total Cost: {self.total_cost:.2f}")
384
+
385
+ # Calculate turn cost separately
386
+ turn_cost = max(0, self.total_cost - self.steps_taken)
387
+ self.turn_cost_label.setText(f"Turn Cost: {turn_cost:.2f}")
388
+
389
+ self.dirty_label.setText(f"Dirty Cells: {self.env.get_dirty_count()}")
390
+ self.nodes_explored_label.setText(f"Nodes Explored: {len(self.explored_cells)}")
391
+ self.nodes_expanded_label.setText(f"Nodes Expanded: {self.total_nodes_expanded}")
392
+ self.comp_time_label.setText(f"Comp Time: {self.total_computation_time:.3f}s")
393
+
394
+ current_algorithm = self.search_combo.currentText()
395
+ self.algorithm_label.setText(f"Algorithm: {current_algorithm}")
396
+
397
+ self.grid_widget.update_path(self.current_path, self.current_direction)
398
+
399
+ def reset_simulation(self):
400
+ self.env.reset()
401
+ self.current_path = []
402
+ self.explored_cells = set()
403
+ self.steps_taken = 0
404
+ self.total_cost = 0
405
+ self.current_direction = None
406
+ self.total_nodes_expanded = 0
407
+ self.total_computation_time = 0
408
+ self.is_running = False
409
+ self.timer.stop()
410
+ self.run_button.setText("Run")
411
+ self.update_display()
412
+
413
+ def next_step(self):
414
+ if self.env.is_clean():
415
+ self.is_running = False
416
+ self.timer.stop()
417
+ self.run_button.setText("Run")
418
+ return
419
+
420
+ # If we don't have a path, find one
421
+ if not self.current_path:
422
+ self.find_path()
423
+ # If still no path after searching, stop
424
+ if not self.current_path:
425
+ self.is_running = False
426
+ self.timer.stop()
427
+ self.run_button.setText("Run")
428
+ return
429
+
430
+ # If we have a path, move along it
431
+ if self.current_path:
432
+ # Move to next position in path
433
+ next_pos = self.current_path.pop(0)
434
+ current_pos = self.env.vacuum_pos
435
+
436
+ # Calculate movement cost with turn cost
437
+ move_cost = 1 # Base movement cost
438
+
439
+ # Determine new direction based on movement
440
+ new_direction = Direction.from_movement(current_pos, next_pos)
441
+
442
+ if self.turn_cost_checkbox.isChecked() and self.current_direction is not None:
443
+ # Add turn cost if direction changed
444
+ if self.current_direction != new_direction:
445
+ turn_cost = 0.5 # 90° turn cost
446
+ move_cost += turn_cost
447
+
448
+ # Update direction
449
+ self.current_direction = new_direction
450
+
451
+ # Update vacuum position
452
+ self.env.vacuum_pos = next_pos
453
+ self.steps_taken += 1
454
+ self.total_cost += move_cost
455
+
456
+ # Clean the cell if it's dirty
457
+ if self.env.clean_cell(next_pos[0], next_pos[1]):
458
+ # If we cleaned a cell, we need to find a new path
459
+ self.current_path = []
460
+
461
+ # Mark cell as explored
462
+ self.env.mark_explored(next_pos[0], next_pos[1])
463
+
464
+ self.update_display()
465
+
466
+ def find_path(self):
467
+ # Get selected search algorithm
468
+ search_type = self.search_combo.currentText()
469
+
470
+ if search_type == "BFS":
471
+ algorithm = "bfs"
472
+ heuristic = "manhattan"
473
+ else:
474
+ algorithm = "a_star"
475
+ if search_type == "A* Manhattan":
476
+ heuristic = "manhattan"
477
+ elif search_type == "A* Euclidean":
478
+ heuristic = "euclidean"
479
+ else: # A* Chebyshev
480
+ heuristic = "chebyshev"
481
+
482
+ # Update search algorithm with current turn cost setting
483
+ self.search.turn_cost_enabled = self.turn_cost_checkbox.isChecked()
484
+
485
+ # Find path to nearest dirty cell
486
+ path, explored, nodes_expanded, computation_time = self.search.find_path_to_nearest_dirty(
487
+ self.env.vacuum_pos, algorithm, heuristic)
488
+
489
+ # Update performance metrics
490
+ self.total_nodes_expanded += nodes_expanded
491
+ self.total_computation_time += computation_time
492
+
493
+ # Update algorithm statistics
494
+ self.algorithm_stats[search_type]["runs"] += 1
495
+ self.algorithm_stats[search_type]["total_nodes"] += nodes_expanded
496
+ self.algorithm_stats[search_type]["total_time"] += computation_time
497
+
498
+ if path:
499
+ # Remove current position from path
500
+ self.current_path = path[1:]
501
+ self.explored_cells.update(explored)
502
+
503
+ # Mark explored cells
504
+ for row, col in explored:
505
+ if (row, col) != self.env.vacuum_pos and self.env.grid[row][col] == CellType.CLEAN:
506
+ self.env.grid[row][col] = CellType.EXPLORED
507
+
508
+ self.update_stats_display()
509
+
510
+ def toggle_run(self):
511
+ self.is_running = not self.is_running
512
+
513
+ if self.is_running:
514
+ self.run_button.setText("Pause")
515
+ self.timer.start(500) # 0.5 second interval for faster visualization
516
+ else:
517
+ self.run_button.setText("Run")
518
+ self.timer.stop()
519
+
520
+ def toggle_turn_cost(self):
521
+ # When turn cost is toggled, we need to recalculate the path
522
+ if self.current_path:
523
+ self.current_path = []