diff --git a/puzzle.py b/puzzle.py new file mode 100644 index 0000000..013ad39 --- /dev/null +++ b/puzzle.py @@ -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): + 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 = [] + # 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!")