Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions puzzle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Q2: Robot Evacuation Planning using Best First Search

# Grid dimensions: 10 rows, 20 columns
# 0 = Walkable (Hallway/Room interior)
# 1 = Wall/Blocked

# Approximated Floor Plan based on image
# Row 4 is the main hallway
# Entry at (8, 4), Exit at (4, 18)

grid = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 0: Top Wall
[1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], # 1: Rooms
[1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], # 2: Rooms
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], # 3: Wall separating rooms from hall
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # 4: Main Hallway (Exit at end)
[1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1], # 5: Structures below hall
[1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1], # 6: More structures
[1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1], # 7: Walls
[1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 8: Entry area (at 8,4)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] # 9: Bottom Wall
]

rows = 10
cols = 20

# Entry and Exit
START = (8, 4) # Entry (Row 8, Col 4)
GOAL = (4, 18) # Exit (Row 4, Col 18)

class Node:
def __init__(self, state, parent=None, action=None, path_cost=0):
self.state = state # (row, col)
self.parent = parent
self.action = action # "Up", "Down", "Left", "Right"
self.path_cost = path_cost

class PriorityQueue:
def __init__(self, f):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PriorityQueue implementation is inefficient.

  • add uses self.data.sort(key=self.f) which takes O(N log N) time for each addition, where N is the number of elements in the queue.
  • pop uses self.data.pop(0) which takes O(N) time as it involves shifting all subsequent elements.

For search algorithms where the frontier can grow large, this quadratic complexity (O(N^2) or worse for add/pop operations over many iterations) will significantly degrade performance. Python's built-in heapq module provides an efficient min-heap implementation, allowing O(log N) for both add (push) and pop operations.

Consider replacing this custom implementation with heapq for better performance and scalability. For example:

import heapq
import itertools # To ensure stable sorting for equal f_values

class PriorityQueue:
    def __init__(self, f):
        self.data = []  # Stores (f_value, entry_id, node) tuples
        self.f = f
        self.counter = itertools.count() # Unique sequence numbers for tie-breaking

    def add(self, node):
        count = next(self.counter)
        heapq.heappush(self.data, (self.f(node), count, node))

    def pop(self):
        return heapq.heappop(self.data)[2] # Return the node

    def top(self):
        return self.data[0][2] # Return the node

    def is_empty(self):
        return len(self.data) == 0

self.data = []
self.f = f

def add(self, node):
self.data.append(node)
self.data.sort(key=self.f)

def pop(self):
return self.data.pop(0)

def top(self):
return self.data[0]

def is_empty(self):
return len(self.data) == 0

class Problem:
def __init__(self, initial, goal, grid):
self.initial = initial
self.goal = goal
self.grid = grid

def is_goal(self, state):
return state == self.goal

def ACTIONS(self, state):
r, c = state
actions = []

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rows and cols variables are currently global. While this works for a single, fixed grid, it can lead to maintainability issues if you need to reuse the Problem class with different grid sizes or in a larger application where globals might conflict.

It's generally better practice to encapsulate grid dimensions within the Problem class itself. You can derive rows and cols from self.grid in the Problem's __init__ method and then use self.rows and self.cols throughout the class and in functions like BEST_FIRST_SEARCH.

Suggestion:

  1. Update Problem.__init__:

    class Problem:
        def __init__(self, initial, goal, grid):
            self.initial = initial
            self.goal = goal
            self.grid = grid
            self.rows = len(grid)
            self.cols = len(grid[0]) if grid else 0 # Handle empty grid case
  2. Update Problem.ACTIONS:

    # ...
            if 0 <= nr < self.rows and 0 <= nc < self.cols:
    # ...
  3. Update BEST_FIRST_SEARCH:

    # ...
        reached = [[None for _ in range(problem.cols)] for _ in range(problem.rows)]
    # ...

This approach makes the Problem class more self-contained and robust.

# Down, Up, Right, Left
# Using simple step cost 1 for all moves
moves = [
("Down", (1, 0)),
("Up", (-1, 0)),
("Right", (0, 1)),
("Left", (0, -1))
]

for name, (dr, dc) in moves:
nr, nc = r + dr, c + dc
# Check boundaries and walls
if 0 <= nr < rows and 0 <= nc < cols:
if self.grid[nr][nc] == 0: # 0 is walkable
actions.append(name)
return actions

def RESULT(self, state, action):
r, c = state
if action == "Down": return (r + 1, c)
if action == "Up": return (r - 1, c)
if action == "Right": return (r, c + 1)
if action == "Left": return (r, c - 1)
return state

def ACTION_COST(self, s, action, s_prime):
return 1 # Uniform cost for grid movement

# Heuristic: Manhattan Distance
def heuristic(node):
r1, c1 = node.state
r2, c2 = GOAL
# h(n) = |x1 - x2| + |y1 - y2|
return abs(r1 - r2) + abs(c1 - c2)

# Evaluation function f(n) = g(n) for Uniform Cost Search (UCS)
def f(node):
# Justification: UCS uses path cost g(n) to find the optimal path.
return node.path_cost

def EXPAND(problem, node):
s = node.state
for action in problem.ACTIONS(s):
s_prime = problem.RESULT(s, action)
cost = node.path_cost + problem.ACTION_COST(s, action, s_prime)
yield Node(state=s_prime, parent=node, action=action, path_cost=cost)

def BEST_FIRST_SEARCH(problem, f):
node = Node(problem.initial)
frontier = PriorityQueue(f)
frontier.add(node)

# 2D reached table
reached = [[None for _ in range(cols)] for _ in range(rows)]
reached[node.state[0]][node.state[1]] = node

explored = 0

while not frontier.is_empty():
node = frontier.pop()
explored += 1

if problem.is_goal(node.state):
return node, explored

for child in EXPAND(problem, node):
r, c = child.state
if reached[r][c] is None or child.path_cost < reached[r][c].path_cost:
reached[r][c] = child
frontier.add(child)

return None, explored

def get_path(node):
path = []
while node:
path.append(node.state)
node = node.parent
return path[::-1]

def print_grid_with_path(grid, path):
print("\nEvaluated Path on Grid:")
path_set = set(path)

# Header
print(" ", end="")
for c in range(cols): print(f"{c%10}", end=" ")
print()

for r in range(rows):
print(f"{r:<2} ", end="")
for c in range(cols):
if (r, c) == START:
print("S", end=" ")
elif (r, c) == GOAL:
print("E", end=" ")
elif (r, c) in path_set:
print(".", end=" ") # Path marker
elif grid[r][c] == 1:
print("#", end=" ") # Wall
else:
print(" ", end=" ") # Empty space
print()

if __name__ == "__main__":
problem = Problem(START, GOAL, grid)

print("Starting Uniform Cost Search (UCS)...")
print(f"Start: {START}, Goal: {GOAL}")

solution, explored = BEST_FIRST_SEARCH(problem, f)

if solution:
path = get_path(solution)
print("\nGoal Found!")
print(f"Path Length: {len(path)}")
print(f"Total Cost: {solution.path_cost}")
print(f"Nodes Explored: {explored}")

print_grid_with_path(grid, path)

print("\nPath Steps:")
curr = solution
steps = []
while curr.parent:
steps.append(f"{curr.action} -> {curr.state}")
curr = curr.parent
for s in reversed(steps):
print(s)

print("\nEvaluation Cost Function Justification:")
print("Function: Path Cost f(n) = g(n)")
print("Reason: Uniform Cost Search expands nodes based on total path cost from the start.")
print("This guarantees shortest path finding in a grid with uniform step costs,")
print("exploring in 'waves' rather than just aiming for the goal.")

else:
print("No path found!")