ai-practical-kit 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ """AIPracticalKit: reusable, beginner-friendly AI practical algorithms."""
2
+
3
+ from .experiments.vacuum_cleaner import solve_vacuum
4
+ from .experiments.astar_8_puzzle import solve_astar, heuristic
5
+ from .experiments.alpha_beta_pruning import solve_alpha_beta
6
+ from .experiments.tower_of_hanoi_bfs import solve_hanoi_bfs
7
+ from .experiments.sudoku_solver import solve_sudoku
8
+ from .experiments.local_beam_tsp import local_beam_tsp, tour_cost
9
+ from .experiments.robot_path_bfs import solve_robot_bfs
10
+ from .experiments.maze_dfs import solve_maze_dfs
11
+ from .experiments.eight_queens import solve_8_queens
12
+ from .experiments.rule_chatbot import respond
13
+ from .experiments.tictactoe_minimax import find_best_move, minimax
14
+
15
+ __version__ = "1.1.0"
16
+
17
+ __all__ = [
18
+ "solve_vacuum", "solve_astar", "heuristic", "solve_alpha_beta",
19
+ "solve_hanoi_bfs", "solve_sudoku", "local_beam_tsp", "tour_cost",
20
+ "solve_robot_bfs", "solve_maze_dfs", "solve_8_queens", "respond",
21
+ "find_best_move", "minimax",
22
+ ]
@@ -0,0 +1,157 @@
1
+ """Text menu for running and inspecting AIPracticalKit experiments."""
2
+
3
+ import importlib
4
+ from pathlib import Path
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+
9
+ SOURCE_DIR = Path(__file__).resolve().parent / "experiments"
10
+
11
+ EXPERIMENTS = [
12
+ (1, "Simple Reflex Agent - Vacuum Cleaner", "vacuum_cleaner.py", "vacuum_cleaner"),
13
+ (2, "A* Search - 8-Puzzle", "astar_8_puzzle.py", "astar_8_puzzle"),
14
+ (3, "Alpha-Beta Pruning", "alpha_beta_pruning.py", "alpha_beta_pruning"),
15
+ (4, "Tower of Hanoi using BFS", "tower_of_hanoi_bfs.py", "tower_of_hanoi_bfs"),
16
+ (5, "Sudoku - Constraint Propagation and Backtracking", "sudoku_solver.py", "sudoku_solver"),
17
+ (6, "Local Beam Search - TSP", "local_beam_tsp.py", "local_beam_tsp"),
18
+ (7, "BFS - Robot Path Planning", "robot_path_bfs.py", "robot_path_bfs"),
19
+ (8, "DFS - Maze Solving", "maze_dfs.py", "maze_dfs"),
20
+ (9, "8-Queens using Backtracking", "eight_queens.py", "eight_queens"),
21
+ (10, "Rule-Based Chatbot - Regex and AND", "rule_chatbot.py", "rule_chatbot"),
22
+ (11, "Tic-Tac-Toe using Minimax", "tictactoe_minimax.py", "tictactoe_minimax"),
23
+ ]
24
+
25
+
26
+ def _select_experiment():
27
+ print("\nAI PRACTICAL KIT - EXPERIMENTS")
28
+ for number, title, _, _ in EXPERIMENTS:
29
+ print(str(number) + ". " + title)
30
+ try:
31
+ number = int(input("Experiment number (0 to cancel): ").strip())
32
+ except ValueError:
33
+ print("Please enter a number.")
34
+ return None
35
+ if number == 0:
36
+ return None
37
+ for item in EXPERIMENTS:
38
+ if item[0] == number:
39
+ return item
40
+ print("Invalid experiment number.")
41
+ return None
42
+
43
+
44
+ def _open_editor(path):
45
+ editor = shutil.which("code")
46
+ try:
47
+ if editor:
48
+ subprocess.Popen([editor, "--reuse-window", str(path)])
49
+ elif sys.platform.startswith("win"):
50
+ subprocess.Popen(["notepad.exe", str(path)])
51
+ elif sys.platform == "darwin":
52
+ subprocess.Popen(["open", "-t", str(path)])
53
+ else:
54
+ opener = shutil.which("xdg-open")
55
+ if opener:
56
+ subprocess.Popen([opener, str(path)])
57
+ else:
58
+ print("Open this source in your editor:\n" + str(path))
59
+ return
60
+ print("Opened source:", path.name)
61
+ except OSError as error:
62
+ print("Could not open the editor:", error)
63
+ print("Source file:", path)
64
+
65
+
66
+ def _copy_source(path):
67
+ source = path.read_text(encoding="utf-8")
68
+ commands = []
69
+ if sys.platform.startswith("win"):
70
+ commands.append(["clip"])
71
+ elif sys.platform == "darwin":
72
+ commands.append(["pbcopy"])
73
+ else:
74
+ commands.extend([["wl-copy"], ["xclip", "-selection", "clipboard"],
75
+ ["xsel", "--clipboard", "--input"]])
76
+
77
+ for command in commands:
78
+ if shutil.which(command[0]):
79
+ try:
80
+ subprocess.run(command, input=source, text=True, check=True)
81
+ print("Copied source to clipboard:", path.name)
82
+ return
83
+ except (OSError, subprocess.CalledProcessError):
84
+ pass
85
+
86
+ try:
87
+ import tkinter
88
+ window = tkinter.Tk()
89
+ window.withdraw()
90
+ window.clipboard_clear()
91
+ window.clipboard_append(source)
92
+ window.update()
93
+ window.destroy()
94
+ print("Copied source to clipboard:", path.name)
95
+ except Exception:
96
+ print("Clipboard copy is unavailable. Open this source and copy it manually:")
97
+ print(path)
98
+
99
+
100
+ def main():
101
+ """Run the installed text menu."""
102
+ while True:
103
+ print("\n" + "=" * 60)
104
+ print(" AI PRACTICAL KIT")
105
+ print(" Run algorithms or inspect full source code")
106
+ print("=" * 60)
107
+ print("1. Run an experiment")
108
+ print("2. View full source code")
109
+ print("3. Open source in an editor")
110
+ print("4. Copy source code")
111
+ print("5. Show import examples")
112
+ print("0. Exit")
113
+ choice = input("Choose an option: ").strip()
114
+
115
+ if choice == "0":
116
+ print("Good luck with your practical examinations!")
117
+ return
118
+
119
+ if choice == "5":
120
+ print("\nfrom ai_practical_kit import solve_astar")
121
+ print('result = solve_astar("123406758")')
122
+ print('print(result["path"])')
123
+ print("\nfrom ai_practical_kit import solve_8_queens")
124
+ print("result = solve_8_queens()")
125
+ print('print(result["total_solutions"])')
126
+ print("\nfrom ai_practical_kit import respond")
127
+ print('print(respond("Tell me about admission and fees"))')
128
+ continue
129
+
130
+ item = _select_experiment()
131
+ if item is None:
132
+ continue
133
+ number, title, filename, module_name = item
134
+ path = SOURCE_DIR / filename
135
+
136
+ if choice == "1":
137
+ module = importlib.import_module("ai_practical_kit.experiments." + module_name)
138
+ try:
139
+ module.run_cli()
140
+ except (KeyboardInterrupt, EOFError):
141
+ print("\nExperiment cancelled.")
142
+ elif choice == "2":
143
+ print("\n" + "=" * 72)
144
+ print(title + " — FULL SOURCE")
145
+ print("=" * 72)
146
+ print(path.read_text(encoding="utf-8"))
147
+ print("=" * 72)
148
+ elif choice == "3":
149
+ _open_editor(path)
150
+ elif choice == "4":
151
+ _copy_source(path)
152
+ else:
153
+ print("Invalid choice. Choose 0 to 5.")
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
File without changes
@@ -0,0 +1,91 @@
1
+ """Experiment 3: Alpha-Beta Pruning on a depth-3 binary game tree."""
2
+
3
+ INFINITY = 10**9
4
+ DEFAULT_LEAVES = [3, 5, 6, 9, 1, 2, 0, -1]
5
+ INTERNAL_NAMES = "ABCDEFG"
6
+
7
+
8
+ def solve_alpha_beta(leaf_values=None):
9
+ """Return minimax value, root branch, evaluated leaves and pruned branches."""
10
+ if leaf_values is None:
11
+ leaf_values = DEFAULT_LEAVES[:]
12
+ leaf_values = list(leaf_values)
13
+ if len(leaf_values) != 8:
14
+ raise ValueError("Enter exactly 8 leaf values.")
15
+
16
+ evaluated = 0
17
+ pruned = []
18
+ best_root_branch = None
19
+
20
+ def node_name(node_index):
21
+ if node_index < 7:
22
+ return INTERNAL_NAMES[node_index]
23
+ return str(leaf_values[node_index - 7])
24
+
25
+ def search(node, depth, is_max, alpha, beta):
26
+ nonlocal evaluated, best_root_branch
27
+
28
+ if depth == 3:
29
+ evaluated += 1
30
+ return leaf_values[node - 7]
31
+
32
+ if is_max:
33
+ best = -INFINITY
34
+ else:
35
+ best = INFINITY
36
+
37
+ for child_number in range(2):
38
+ child = 2 * node + 1 + child_number
39
+ score = search(child, depth + 1, not is_max, alpha, beta)
40
+
41
+ if is_max:
42
+ if score > best:
43
+ best = score
44
+ if node == 0:
45
+ best_root_branch = node_name(child)
46
+ if best > alpha:
47
+ alpha = best
48
+ else:
49
+ if score < best:
50
+ best = score
51
+ if best < beta:
52
+ beta = best
53
+
54
+ if alpha >= beta:
55
+ if child_number == 0:
56
+ skipped = 2 * node + 2
57
+ if depth + 1 == 3:
58
+ pruned.append("leaf " + node_name(skipped))
59
+ else:
60
+ pruned.append("subtree " + node_name(skipped))
61
+ break
62
+
63
+ return best
64
+
65
+ value = search(0, 0, True, -INFINITY, INFINITY)
66
+ return {"value": value, "best_branch": best_root_branch,
67
+ "leaves_evaluated": evaluated, "pruned": pruned,
68
+ "leaf_values": leaf_values}
69
+
70
+
71
+ def run_cli():
72
+ print("\nEXPERIMENT 3: ALPHA-BETA PRUNING")
73
+ print("Enter 8 leaf values from left to right, or press Enter for the practical example.")
74
+ raw = input("Leaf values [3 5 6 9 1 2 0 -1]: ").strip()
75
+ try:
76
+ values = DEFAULT_LEAVES if raw == "" else [int(x) for x in raw.split()]
77
+ result = solve_alpha_beta(values)
78
+ except ValueError as error:
79
+ print("Input error:", error)
80
+ return
81
+ print("Minimax value at root:", result["value"])
82
+ print("Best branch for MAX:", result["best_branch"])
83
+ print("Leaves evaluated:", result["leaves_evaluated"], "out of 8")
84
+ if result["pruned"]:
85
+ print("Pruned:", ", ".join(result["pruned"]))
86
+ else:
87
+ print("Pruned: none")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ run_cli()
@@ -0,0 +1,116 @@
1
+ """Experiment 2: A* Search for the 8-Puzzle using Manhattan distance."""
2
+
3
+ import heapq
4
+
5
+ GOAL = "123456780"
6
+
7
+
8
+ def heuristic(state):
9
+ """Sum of Manhattan distances; the blank ('0') is ignored."""
10
+ distance = 0
11
+ for index in range(9):
12
+ if state[index] != "0":
13
+ goal_index = int(state[index]) - 1
14
+ distance += abs(index // 3 - goal_index // 3)
15
+ distance += abs(index % 3 - goal_index % 3)
16
+ return distance
17
+
18
+
19
+ def is_solvable(state):
20
+ """For a 3x3 puzzle, the inversion count must be even."""
21
+ numbers = [int(ch) for ch in state if ch != "0"]
22
+ inversions = 0
23
+ for i in range(len(numbers)):
24
+ for j in range(i + 1, len(numbers)):
25
+ if numbers[i] > numbers[j]:
26
+ inversions += 1
27
+ return inversions % 2 == 0
28
+
29
+
30
+ def solve_astar(start):
31
+ """Solve a valid 8-puzzle string. Return path, moves and expanded count."""
32
+ start = "".join(str(x) for x in start) if not isinstance(start, str) else start.strip().replace(" ", "")
33
+ if len(start) != 9 or sorted(start) != list("012345678"):
34
+ raise ValueError("State must contain digits 0 through 8 exactly once.")
35
+ if not is_solvable(start):
36
+ return {"found": False, "path": [], "moves": None, "expanded": 0}
37
+
38
+ # Entries are (f, g, state); smallest f is removed first.
39
+ open_queue = []
40
+ heapq.heappush(open_queue, (heuristic(start), 0, start))
41
+ best_g = {start: 0}
42
+ parent = {start: None}
43
+ expanded = 0
44
+ directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
45
+
46
+ while open_queue:
47
+ f, current_g, current = heapq.heappop(open_queue)
48
+ if current_g != best_g.get(current):
49
+ continue # An older, worse queue entry.
50
+
51
+ expanded += 1
52
+ if current == GOAL:
53
+ path = []
54
+ state = current
55
+ while state is not None:
56
+ path.append(state)
57
+ state = parent[state]
58
+ path.reverse()
59
+ return {"found": True, "path": path, "moves": len(path) - 1,
60
+ "expanded": expanded}
61
+
62
+ blank = current.index("0")
63
+ row = blank // 3
64
+ col = blank % 3
65
+
66
+ for dr, dc in directions:
67
+ nr = row + dr
68
+ nc = col + dc
69
+ if nr < 0 or nr >= 3 or nc < 0 or nc >= 3:
70
+ continue
71
+
72
+ new_blank = nr * 3 + nc
73
+ chars = list(current)
74
+ chars[blank], chars[new_blank] = chars[new_blank], chars[blank]
75
+ next_state = "".join(chars)
76
+ new_g = current_g + 1
77
+
78
+ if new_g < best_g.get(next_state, 10**9):
79
+ best_g[next_state] = new_g
80
+ parent[next_state] = current
81
+ heapq.heappush(open_queue,
82
+ (new_g + heuristic(next_state), new_g, next_state))
83
+
84
+ return {"found": False, "path": [], "moves": None, "expanded": expanded}
85
+
86
+
87
+ def print_board(state):
88
+ for i in range(9):
89
+ print(("_" if state[i] == "0" else state[i]), end=" ")
90
+ if i % 3 == 2:
91
+ print()
92
+
93
+
94
+ def run_cli():
95
+ print("\nEXPERIMENT 2: A* SEARCH FOR 8-PUZZLE")
96
+ raw = input("Enter 9 digits (0 = blank), e.g. 1 2 3 4 0 6 7 5 8: ")
97
+ start = "".join(raw.split())
98
+ try:
99
+ result = solve_astar(start)
100
+ except ValueError as error:
101
+ print("Input error:", error)
102
+ return
103
+ if not result["found"]:
104
+ print("No solution exists for this puzzle.")
105
+ return
106
+ print("\nSolution path:")
107
+ for step, state in enumerate(result["path"]):
108
+ print("\nStep", step, "(g =", step, ", h =", heuristic(state),
109
+ ", f =", step + heuristic(state), ")")
110
+ print_board(state)
111
+ print("Number of moves:", result["moves"])
112
+ print("Nodes expanded:", result["expanded"])
113
+
114
+
115
+ if __name__ == "__main__":
116
+ run_cli()
@@ -0,0 +1,62 @@
1
+ """Experiment 9: Backtracking for the 8-Queens constraint problem."""
2
+
3
+ SIZE = 8
4
+
5
+
6
+ def solve_8_queens():
7
+ """Return the first solution and total number of solutions (92)."""
8
+ board = [-1] * SIZE
9
+ first_solution = None
10
+ solution_count = 0
11
+
12
+ def is_safe(row, col):
13
+ for previous_row in range(row):
14
+ if board[previous_row] == col:
15
+ return False
16
+ if abs(board[previous_row] - col) == abs(previous_row - row):
17
+ return False
18
+ return True
19
+
20
+ def solve(row):
21
+ nonlocal first_solution, solution_count
22
+ if row == SIZE:
23
+ solution_count += 1
24
+ if first_solution is None:
25
+ first_solution = board[:]
26
+ return
27
+
28
+ for col in range(SIZE):
29
+ if is_safe(row, col):
30
+ board[row] = col
31
+ solve(row + 1)
32
+ board[row] = -1 # Backtrack and try another column.
33
+
34
+ solve(0)
35
+ return {"solution": first_solution, "total_solutions": solution_count}
36
+
37
+
38
+ def run_cli():
39
+ print("\nEXPERIMENT 9: 8-QUEENS USING BACKTRACKING")
40
+ result = solve_8_queens()
41
+ board = result["solution"]
42
+ if board is None:
43
+ print("No solution exists.")
44
+ return
45
+
46
+ print("First solution (Q = queen, . = empty):")
47
+ for row in range(SIZE):
48
+ for col in range(SIZE):
49
+ if board[row] == col:
50
+ print("Q", end=" ")
51
+ else:
52
+ print(".", end=" ")
53
+ print()
54
+
55
+ print("Queen positions (row, column; numbering starts at 1):")
56
+ for row in range(SIZE):
57
+ print("Queen", row + 1, ": (", row + 1, ", ", board[row] + 1, ")", sep="")
58
+ print("Total solutions:", result["total_solutions"])
59
+
60
+
61
+ if __name__ == "__main__":
62
+ run_cli()
@@ -0,0 +1,116 @@
1
+ """Experiment 6: Local Beam Search for the Traveling Salesman Problem."""
2
+
3
+ import itertools
4
+ import math
5
+ import random
6
+
7
+
8
+ def tour_cost(tour, distance):
9
+ """Total cost of a tour, including the return to city 0."""
10
+ total = 0
11
+ n = len(tour)
12
+ for i in range(n - 1):
13
+ total += distance[tour[i]][tour[i + 1]]
14
+ total += distance[tour[-1]][tour[0]]
15
+ return total
16
+
17
+
18
+ def local_beam_tsp(distance, beam_width=3, max_iterations=20, seed=42):
19
+ """Return the best tour found by Local Beam Search.
20
+
21
+ City 0 is fixed at the beginning of each tour. `distance` is a square matrix.
22
+ """
23
+ n = len(distance)
24
+ if n < 3 or any(len(row) != n for row in distance):
25
+ raise ValueError("Distance matrix must be square and contain at least 3 cities.")
26
+ if beam_width < 1 or max_iterations < 1:
27
+ raise ValueError("Beam width and iterations must both be at least 1.")
28
+ if any(distance[i][j] < 0 for i in range(n) for j in range(n)):
29
+ raise ValueError("Distances cannot be negative.")
30
+
31
+ beam_width = min(beam_width, math.factorial(n - 1))
32
+ rng = random.Random(seed)
33
+ city_list = list(range(1, n))
34
+ beam = []
35
+ seen = set()
36
+
37
+ # Create distinct initial tours in a repeatable random order.
38
+ attempts = 0
39
+ while len(beam) < beam_width and attempts < beam_width * 50:
40
+ rng.shuffle(city_list)
41
+ tour = tuple([0] + city_list[:])
42
+ if tour not in seen:
43
+ seen.add(tour)
44
+ beam.append(tour)
45
+ attempts += 1
46
+
47
+ # Fill any remaining slots systematically (small practical inputs).
48
+ if len(beam) < beam_width:
49
+ for order in itertools.permutations(range(1, n)):
50
+ tour = tuple([0] + list(order))
51
+ if tour not in seen:
52
+ seen.add(tour)
53
+ beam.append(tour)
54
+ if len(beam) == beam_width:
55
+ break
56
+
57
+ best_tour = min(beam, key=lambda tour: tour_cost(tour, distance))
58
+ best_cost = tour_cost(best_tour, distance)
59
+ history = [best_cost]
60
+
61
+ for _ in range(max_iterations):
62
+ candidates = {}
63
+ for tour in beam:
64
+ for i in range(1, n - 1):
65
+ for j in range(i + 1, n):
66
+ neighbour = list(tour)
67
+ neighbour[i], neighbour[j] = neighbour[j], neighbour[i]
68
+ neighbour = tuple(neighbour)
69
+ candidates[neighbour] = tour_cost(neighbour, distance)
70
+
71
+ if not candidates:
72
+ break
73
+
74
+ ordered = sorted(candidates.items(), key=lambda item: (item[1], item[0]))
75
+ next_beam = [tour for tour, cost_value in ordered[:beam_width]]
76
+ beam = next_beam
77
+
78
+ if ordered[0][1] < best_cost:
79
+ best_tour = ordered[0][0]
80
+ best_cost = ordered[0][1]
81
+ history.append(best_cost)
82
+
83
+ return {"tour": list(best_tour), "cost": best_cost,
84
+ "iterations": len(history) - 1, "best_cost_history": history}
85
+
86
+
87
+ def run_cli():
88
+ print("\nEXPERIMENT 6: LOCAL BEAM SEARCH FOR TSP")
89
+ try:
90
+ n = int(input("Enter number of cities (3-10 recommended): "))
91
+ if n < 3 or n > 10:
92
+ raise ValueError("Use between 3 and 10 cities for this practical.")
93
+ distance = []
94
+ print("Enter the distance matrix, one row per line:")
95
+ for _ in range(n):
96
+ row = [float(x) for x in input().split()]
97
+ if len(row) != n:
98
+ raise ValueError("Each distance row must contain exactly n values.")
99
+ distance.append(row)
100
+ beam_width = int(input("Enter beam width K: "))
101
+ iterations = int(input("Enter maximum iterations: "))
102
+ result = local_beam_tsp(distance, beam_width, iterations)
103
+ except ValueError as error:
104
+ print("Input error:", error)
105
+ return
106
+
107
+ tour = result["tour"]
108
+ print("\nBest tour found:")
109
+ print(" -> ".join(str(city) for city in tour + [tour[0]]))
110
+ print("Best tour cost:", round(result["cost"], 3))
111
+ print("Iterations completed:", result["iterations"])
112
+ print("Note: Local Beam Search is heuristic and does not guarantee the global optimum.")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ run_cli()
@@ -0,0 +1,92 @@
1
+ """Experiment 8: DFS for Maze Solving."""
2
+
3
+
4
+ def solve_maze_dfs(maze):
5
+ """Find a path from S to E by DFS; return path and a marked maze."""
6
+ if not maze or not maze[0]:
7
+ raise ValueError("Maze cannot be empty.")
8
+ rows = len(maze)
9
+ cols = len(maze[0])
10
+ if any(len(row) != cols for row in maze):
11
+ raise ValueError("All maze rows must have the same length.")
12
+
13
+ start = None
14
+ goal = None
15
+ for r in range(rows):
16
+ for c in range(cols):
17
+ if maze[r][c] == "S":
18
+ if start is not None:
19
+ raise ValueError("Maze must contain exactly one S.")
20
+ start = (r, c)
21
+ elif maze[r][c] == "E":
22
+ if goal is not None:
23
+ raise ValueError("Maze must contain exactly one E.")
24
+ goal = (r, c)
25
+ if start is None or goal is None:
26
+ raise ValueError("Maze must contain one S (start) and one E (exit).")
27
+
28
+ visited = set()
29
+ path = []
30
+ # Same order as the practical: Right, Down, Left, Up.
31
+ directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
32
+
33
+ def dfs(r, c):
34
+ if r < 0 or r >= rows or c < 0 or c >= cols:
35
+ return False
36
+ if maze[r][c] == "#" or (r, c) in visited:
37
+ return False
38
+
39
+ visited.add((r, c))
40
+ path.append((r, c))
41
+ if (r, c) == goal:
42
+ return True
43
+
44
+ for dr, dc in directions:
45
+ if dfs(r + dr, c + dc):
46
+ return True
47
+
48
+ path.pop() # Dead end: remove this cell from the current path.
49
+ return False
50
+
51
+ found = dfs(start[0], start[1])
52
+ if not found:
53
+ return {"found": False, "path": [], "maze": list(maze),
54
+ "visited_count": len(visited)}
55
+
56
+ marked = [list(row) for row in maze]
57
+ for r, c in path:
58
+ if marked[r][c] == ".":
59
+ marked[r][c] = "*"
60
+ return {"found": True, "path": path,
61
+ "maze": ["".join(row) for row in marked],
62
+ "visited_count": len(visited)}
63
+
64
+
65
+ def run_cli():
66
+ print("\nEXPERIMENT 8: DFS MAZE SOLVER")
67
+ try:
68
+ rows, cols = [int(x) for x in input("Enter rows and columns: ").split()]
69
+ if rows < 1 or cols < 1 or rows > 50 or cols > 50:
70
+ raise ValueError("Rows and columns must be between 1 and 50.")
71
+ print("Enter each maze row using S=start, E=exit, #=wall, .=free:")
72
+ maze = [input().strip() for _ in range(rows)]
73
+ if any(len(row) != cols for row in maze):
74
+ raise ValueError("Each row must contain exactly the given number of columns.")
75
+ result = solve_maze_dfs(maze)
76
+ except ValueError as error:
77
+ print("Input error:", error)
78
+ return
79
+
80
+ if not result["found"]:
81
+ print("No path exists in the maze.")
82
+ return
83
+ print("\nPath found (DFS does not guarantee shortest path):")
84
+ for r, c in result["path"]:
85
+ print("(" + str(r + 1) + "," + str(c + 1) + ")", end=" ")
86
+ print("\n\nMaze with path:")
87
+ for row in result["maze"]:
88
+ print(" ".join(row))
89
+
90
+
91
+ if __name__ == "__main__":
92
+ run_cli()