diff --git a/assignment1.py b/assignment1.py new file mode 100644 index 000000000..0784d8138 --- /dev/null +++ b/assignment1.py @@ -0,0 +1,229 @@ +from time import time +from search import * +from assignment1aux import * +from heapq import heappush, heappop + +def read_initial_state_from_file(filename): + # Task 1 + # Return an initial state constructed using a configuration in a file. + # Replace the line below with your code. + temTuple = () + with open(filename, 'r') as file: + lin = file.readlines() + lines = [line.strip() for line in lin] + height=int(lines[1]) + width=int(lines[0]) + map = [[''] * height for _ in range(width)] + for i in range(2,len(lines)): + temp=lines[i].split(',') + map[int(temp[0])][int(temp[1])] = 'rock' + tuple_map=tuple(tuple(row) for row in map) + temTuple = (tuple_map, None, None) + visualise(temTuple) + return temTuple + +class ZenPuzzleGarden(Problem): + def __init__(self, initial): + if type(initial) is str: + super().__init__(read_initial_state_from_file(initial)) + else: + super().__init__(initial) + + def actions(self, state): + # Task 2.1 + # Return a list of all allowed actions in a given state. + # Replace the line below with your code. + action=[] + #if no more action, start to find the new way + if state[1] is None: + for i in range(len(state[0][0])): + if state[0][0][i]=='': + action.append(((0,i),'down')) + if state[0][len(state[0])-1][i]=='': + action.append(((len(state[0])-1,i),'up')) + for i in range(len(state[0])): + if state[0][i][0]=='': + action.append(((i,0),'right')) + if state[0][i][len(state[0][i])-1]=='': + action.append(((i,len(state[0][i])-1),'left')) + #if there is a point, find the new way + else: + direction=state[2] + x, y = state[1] + height = len(state[0]) + width=len(state[0][0]) + if direction in ['up', 'down']: + possible_dirs = ['left', 'right'] + else: + possible_dirs = ['up', 'down'] + for new_dir in possible_dirs: + dx, dy = 0, 0 + if new_dir == 'up': dx = -1 + elif new_dir == 'down': dx = 1 + elif new_dir == 'left': dy = -1 + else: dy = 1 + fx, fy = x + dx, y + dy + if 0 <= fx < height and 0 <= fy < width: + if state[0][fx][fy] == '': + action.append(((x, y), new_dir)) + else: + action.append(((x, y), new_dir)) + return action + + def result(self, state, action): + # Task 2.2 + # Return a new state resulting from a given action being applied to a given state. + # Replace the line below with your code. + listState = list(list(row) for row in state[0]) + point=list(action[0]) + direction=action[1] + while point[0]<=len(state[0])-1 and point[1]>=0 and point[1]<=len(state[0][0])-1 and point[0]>=0: + #if the point is out of the map, return the state + if direction=='down': + if point[0]+1>len(state[0])-1: + listState[point[0]][point[1]]=direction + return (tuple(tuple(row) for row in listState), None, None) + if state[0][point[0]+1][point[1]]!='': + break + listState[point[0]][point[1]]='down' + point[0]+=1 + elif direction=='up': + if point[0]-1<0: + listState[point[0]][point[1]]=direction + return (tuple(tuple(row) for row in listState), None, None) + if state[0][point[0]-1][point[1]]!='': + break + listState[point[0]][point[1]]='up' + point[0]-=1 + elif direction=='right': + if point[1]+1>len(state[0][0])-1: + listState[point[0]][point[1]]=direction + return (tuple(tuple(row) for row in listState), None, None) + if state[0][point[0]][point[1]+1]!='': + break + listState[point[0]][point[1]]='right' + point[1]+=1 + elif direction=='left': + if point[1]-1<0: + listState[point[0]][point[1]]=direction + return (tuple(tuple(row) for row in listState), None, None) + if state[0][point[0]][point[1]-1]!='': + break + listState[point[0]][point[1]]='left' + point[1]-=1 + return (tuple(tuple(row) for row in listState), tuple(point), direction) + + def goal_test(self, state): + # Task 2.3 + # Return a boolean value indicating if a given state is solved. + # Replace the line below with your code. + map=list(state[0]) + for i in map: + if '' in i: + return False + return True + +def findhuristic(node): + state=node.state + # Add a penalty for disconnected empty regions to encourage filling contiguous areas + def count_disconnected_regions(grid): + visited = set() + regions = 0 + def dfs(x, y): + if (x, y) in visited or grid[x][y] != '': + return + visited.add((x, y)) + for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + nx, ny = x + dx, y + dy + if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]): + dfs(nx, ny) + + for i in range(len(grid)): + for j in range(len(grid[0])): + if grid[i][j] == '' and (i, j) not in visited: + regions += 1 + dfs(i, j) + return regions + + disconnected_regions = count_disconnected_regions(state[0]) + + # Combine the two components into the heuristic + return disconnected_regions + + +# Task 3 +# Implement an A* heuristic cost function and assign it to the variable below. +astar_heuristic_cost = lambda n: findhuristic(n) +def beam_search(problem, f, beam_width): + # Task 4 + # Implement a beam-width version A* search. + # Return a search node containing a solved state. + # Experiment with the beam width in the test code to find a solution. + # Replace the line below with your code. + f = memoize(f, 'f') + node = Node(problem.initial) + frontier = [] + frontier.append(node) + explored = set() + + while frontier: + for node in frontier: + if problem.goal_test(node.state): + return node + next_frontier = [] + for node in frontier: + explored.add(node.state) + for child in node.expand(problem): + if child.state not in explored and child not in frontier: + next_frontier.append(child) + next_frontier.sort(key=f) + frontier = next_frontier[:beam_width] + return None + + +if __name__ == "__main__": + + # Task 1 test code + print('The loaded initial state is visualised below.') + garden = ZenPuzzleGarden(initial='assignment1config.txt') + + # Task 2 test code + print('Running breadth-first graph search.') + before_time = time() + node = breadth_first_graph_search(garden) + after_time = time() + print(f'Breadth-first graph search took {after_time - before_time} seconds.') + if node: + print(f'Its solution with a cost of {node.path_cost} is animated below.') + animate(node) + else: + print('No solution was found.') + + + # Task 3 test code + + print('Running A* search.') + before_time = time() + node = astar_search(garden, astar_heuristic_cost) + after_time = time() + print(f'A* search took {after_time - before_time} seconds.') + if node: + print(f'Its solution with a cost of {node.path_cost} is animated below.') + animate(node) + else: + print('No solution was found.') + + + # Task 4 test code + + print('Running beam search.') + before_time = time() + node = beam_search(garden, lambda n: n.path_cost + astar_heuristic_cost(n), 500) + after_time = time() + print(f'Beam search took {after_time - before_time} seconds.') + if node: + print(f'Its solution with a cost of {node.path_cost} is animated below.') + animate(node) + else: + print('No solution was found.') + diff --git a/assignment1aux.py b/assignment1aux.py new file mode 100644 index 000000000..208247d6d --- /dev/null +++ b/assignment1aux.py @@ -0,0 +1,51 @@ +from os import system +from time import sleep + +def visualise(state): + map = state[0] + position = state[1] + move = state[2] + height = len(map) + width = len(map[0]) + print('\n ', *['\u2581' for _ in range(width)], ' ', sep='') + for i in range(height): + print('\u2595', end='') + for j in range(width): + if map[i][j] == 'rock': + print('\u2588', end='') + elif map[i][j] == 'left': + print('\u25c2', end='') + elif map[i][j] == 'up': + print('\u25b4', end='') + elif map[i][j] == 'right': + print('\u25b8', end='') + elif map[i][j] == 'down': + print('\u25be', end='') + elif position == (i, j): + if move == 'right': + print('\u2520', end='') + elif move == 'left': + print('\u2528', end='') + elif move == 'down': + print('\u252f', end='') + elif move == 'up': + print('\u2537', end='') + else: + print('\u25cb', end='') + elif not map[i][j]: + print(' ', end='') + else: + print() + print(f"Unexpected tile representation encountered: '{map[i][j]}'.") + print("Accepted tile representations are '', 'rock', 'left', 'right', 'up', and 'down'.") + raise ValueError(map[i][j]) + print('\u258f') + print(' ', *['\u2594' for _ in range(width)], ' \n', sep='') + +def animate(node): + for node_i in node.path()[:-1]: + visualise(node_i.state) + sleep(2) + for _ in range(len(node_i.state[0]) + 4): + system('tput cuu1') + visualise(node.state) diff --git a/assignment1config.txt b/assignment1config.txt new file mode 100644 index 000000000..eab884fc9 --- /dev/null +++ b/assignment1config.txt @@ -0,0 +1,4 @@ +4 +5 +2,2 +2,3 \ No newline at end of file diff --git a/search.ipynb b/search.ipynb index caf231dcc..a6026ba9d 100644 --- a/search.ipynb +++ b/search.ipynb @@ -123,33 +123,45 @@ "text/html": [ "\n", - "\n", + "\n", "\n", "
\n", "