From 09469a82b65e9565c7e8b827e628d1ae43f925c3 Mon Sep 17 00:00:00 2001 From: AyeshaK <124094029+Arrowhorse@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:39:59 -0400 Subject: [PATCH] Add Pacman Search Agents project --- multiAgents.py | 392 +++++++++++++++++++++++++++++ searchAgents.py | 640 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1032 insertions(+) create mode 100644 multiAgents.py create mode 100644 searchAgents.py diff --git a/multiAgents.py b/multiAgents.py new file mode 100644 index 0000000..939170e --- /dev/null +++ b/multiAgents.py @@ -0,0 +1,392 @@ +# multiAgents.py +# -------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + +# Authors: Ayesha Kazi, Mahina Khan + +from util import manhattanDistance +from game import Directions +import random, util + +from game import Agent +from pacman import GameState + +class ReflexAgent(Agent): + """ + A reflex agent chooses an action at each choice point by examining + its alternatives via a state evaluation function. + + The code below is provided as a guide. You are welcome to change + it in any way you see fit, so long as you don't touch our method + headers. + """ + + + def getAction(self, gameState: GameState): + """ + You do not need to change this method, but you're welcome to. + + getAction chooses among the best options according to the evaluation function. + + Just like in the previous project, getAction takes a GameState and returns + some Directions.X for some X in the set {NORTH, SOUTH, WEST, EAST, STOP} + """ + # Collect legal moves and successor states + legalMoves = gameState.getLegalActions() + + # Choose one of the best actions + scores = [self.evaluationFunction(gameState, action) for action in legalMoves] + bestScore = max(scores) + bestIndices = [index for index in range(len(scores)) if scores[index] == bestScore] + chosenIndex = random.choice(bestIndices) # Pick randomly among the best + + "Add more of your code here if you want to" + + return legalMoves[chosenIndex] + + def evaluationFunction(self, currentGameState: GameState, action): + """ + Design a better evaluation function here. + + The evaluation function takes in the current and proposed successor + GameStates (pacman.py) and returns a number, where higher numbers are better. + + The code below extracts some useful information from the state, like the + remaining food (newFood) and Pacman position after moving (newPos). + newScaredTimes holds the number of moves that each ghost will remain + scared because of Pacman having eaten a power pellet. + + Print out these variables to see what you're getting, then combine them + to create a masterful evaluation function. + """ + # Useful information you can extract from a GameState (pacman.py) + successorGameState = currentGameState.generatePacmanSuccessor(action) + newPos = successorGameState.getPacmanPosition() + newFood = successorGameState.getFood() + newGhostStates = successorGameState.getGhostStates() + newScaredTimes = [ghostState.scaredTimer for ghostState in newGhostStates] + + "*** YOUR CODE HERE ***" + score = successorGameState.getScore() + + foodList = newFood.asList() + if foodList: + minFoodDist = min(manhattanDistance(newPos, food) for food in foodList) + score += 15.0 / (minFoodDist + 1) + + for i, ghost in enumerate(newGhostStates): + ghostPos = ghost.getPosition() + ghostDist = manhattanDistance(newPos, ghostPos) + + if newScaredTimes[i] > 0: + score += 30.0 / (ghostDist + 1) + else: + if ghostDist < 2: + score -= 300 + else: + score -= 5.0 / (ghostDist + 1) + + if action == Directions.STOP: + score -= 15 + + return score + + +def scoreEvaluationFunction(currentGameState: GameState): + """ + This default evaluation function just returns the score of the state. + The score is the same one displayed in the Pacman GUI. + + This evaluation function is meant for use with adversarial search agents + (not reflex agents). + """ + return currentGameState.getScore() + +class MultiAgentSearchAgent(Agent): + """ + This class provides some common elements to all of your + multi-agent searchers. Any methods defined here will be available + to the MinimaxPacmanAgent, AlphaBetaPacmanAgent & ExpectimaxPacmanAgent. + + You *do not* need to make any changes here, but you can if you want to + add functionality to all your adversarial search agents. Please do not + remove anything, however. + + Note: this is an abstract class: one that should not be instantiated. It's + only partially specified, and designed to be extended. Agent (game.py) + is another abstract class. + """ + + def __init__(self, evalFn = 'scoreEvaluationFunction', depth = '2'): + self.index = 0 # Pacman is always agent index 0 + self.evaluationFunction = util.lookup(evalFn, globals()) + self.depth = int(depth) + +class MinimaxAgent(MultiAgentSearchAgent): + """ + Your minimax agent (question 2) + """ + + def getAction(self, gameState: GameState): + """ + Returns the minimax action from the current gameState using self.depth + and self.evaluationFunction. + + Here are some method calls that might be useful when implementing minimax. + + gameState.getLegalActions(agentIndex): + Returns a list of legal actions for an agent + agentIndex=0 means Pacman, ghosts are >= 1 + + gameState.generateSuccessor(agentIndex, action): + Returns the successor game state after an agent takes an action + + gameState.getNumAgents(): + Returns the total number of agents in the game + + gameState.isWin(): + Returns whether or not the game state is a winning state + + gameState.isLose(): + Returns whether or not the game state is a losing state + """ + "*** YOUR CODE HERE ***" + numAgents = gameState.getNumAgents() + + def value(state, agentIndex, depthLeft): + """ + Recursive minimax value. + - state: GameState at this node + - agentIndex: whose turn (0 = Pacman, 1..N-1 = ghosts) + - depthLeft: remaining plies (Pacman+all ghosts) + """ + + if depthLeft == 0 or state.isWin() or state.isLose(): + return self.evaluationFunction(state) + + actions = state.getLegalActions(agentIndex) + if not actions: + return self.evaluationFunction(gameState) + if agentIndex == 0: + best = float('-inf') + for a in actions: + success = state.generateSuccessor(0, a) + best = max(best, value(success, 1, depthLeft)) + return best + + else: + best = float('inf') + if agentIndex < numAgents - 1: + nextAgent = agentIndex + 1 + nextDepthLeft = depthLeft + else: + nextAgent = 0 + nextDepthLeft = depthLeft - 1 + + for a in actions: + success = state.generateSuccessor(agentIndex, a) + best = min(best, value(success, nextAgent, nextDepthLeft)) + return best + actions = gameState.getLegalActions(0) + bestVal = float('-inf') + bestAction = None + for a in actions: + succ = gameState.generateSuccessor(0, a) + v = value(succ, 1, self.depth) + if v > bestVal: + bestVal = v + bestAction = a + + return bestAction + + +class AlphaBetaAgent(MultiAgentSearchAgent): + """ + Your minimax agent with alpha-beta pruning (question 3) + """ + + def getAction(self, gameState: GameState): + """ + Returns the minimax action using self.depth and self.evaluationFunction + """ + "*** YOUR CODE HERE ***" + numAgents = gameState.getNumAgents() + + def value(state, agentIndex, depthLeft, alpha, beta): + """ + Recursive alpha-beta value. + """ + if depthLeft == 0 or state.isWin() or state.isLose(): + return self.evaluationFunction(state) + + actions = state.getLegalActions(agentIndex) + if not actions: + return self.evaluationFunction(state) + # max + if agentIndex == 0: + v = float('-inf') + for a in actions: + succ = state.generateSuccessor(0, a) + v = max(v, value(succ, 1, depthLeft, alpha, beta)) + # strict pruning + if v > beta: + return v + alpha = max(alpha, v) + return v + # min + else: + v = float('inf') + # Determine next (agentIndex, depthLeft) + if agentIndex < numAgents - 1: + nextAgent = agentIndex + 1 + nextDepthLeft = depthLeft + else: + nextAgent = 0 + nextDepthLeft = depthLeft - 1 + + for a in actions: + succ = state.generateSuccessor(agentIndex, a) + v = min(v, value(succ, nextAgent, nextDepthLeft, alpha, beta)) + if v < alpha: + return v + beta = min(beta, v) + return v + + actions = gameState.getLegalActions(0) + + bestVal = float('-inf') + bestAction = None + alpha, beta = float('-inf'), float('inf') + + for a in actions: + succ = gameState.generateSuccessor(0, a) + v = value(succ, 1, self.depth, alpha, beta) + if v > bestVal: + bestVal = v + bestAction = a + if bestVal > beta: + break + alpha = max(alpha, bestVal) + + return bestAction + +class ExpectimaxAgent(MultiAgentSearchAgent): + """ + Your expectimax agent (question 4) + """ + + def getAction(self, gameState: GameState): + """ + Returns the expectimax action using self.depth and self.evaluationFunction + + All ghosts should be modeled as choosing uniformly at random from their + legal moves. + """ + "*** YOUR CODE HERE ***" + numAgents = gameState.getNumAgents() + + def value(state, agentIndex, depthLeft): + """ + Recursive expectimax value. + """ + if depthLeft == 0 or state.isWin() or state.isLose(): + return self.evaluationFunction(state) + + actions = state.getLegalActions(agentIndex) + if not actions: + return self.evaluationFunction(state) + # max + if agentIndex == 0: + best = float('-inf') + for a in actions: + succ = state.generateSuccessor(0, a) + best = max(best, value(succ, 1, depthLeft)) + return best + # ghosts modeled as choosing uniformly at random + else: + if agentIndex < numAgents - 1: + nextAgent = agentIndex + 1 + nextDepthLeft = depthLeft + else: + nextAgent = 0 + nextDepthLeft = depthLeft - 1 + + prob = 1.0 / len(actions) + expected = 0.0 + for a in actions: + succ = state.generateSuccessor(agentIndex, a) + expected += prob * value(succ, nextAgent, nextDepthLeft) + return expected + + actions = gameState.getLegalActions(0) + if not actions: + from game import Directions + return Directions.STOP + + bestVal = float('-inf') + bestAction = None + for a in actions: + succ = gameState.generateSuccessor(0, a) + v = value(succ, 1, self.depth) + if v > bestVal: + bestVal = v + bestAction = a + return bestAction + + +def betterEvaluationFunction(currentGameState: GameState): + """ + Your extreme ghost-hunting, pellet-nabbing, food-gobbling, unstoppable + evaluation function (question 5). + + DESCRIPTION: Pacman's game score is combined with weighted heuristics + which include the food, capsules and ghost distance. Closeness to food and scared ghosts is rewarded + while active nearby ghosts are penalized. + """ + "*** YOUR CODE HERE ***" + pacPos = currentGameState.getPacmanPosition() + foodList = currentGameState.getFood().asList() + capsules = currentGameState.getCapsules() + ghostStates = currentGameState.getGhostStates() + + total = currentGameState.getScore() + + if foodList: + nearestFood = min(manhattanDistance(pacPos, f) for f in foodList) + total += 17.3 / (nearestFood + 1.2) + total -= 3.7 * len(foodList) + + if capsules: + nearestCapsule = min(manhattanDistance(pacPos, c) for c in capsules) + total += 11.9 / (nearestCapsule + 1.1) + total -= 18.4 * len(capsules) + + for ghost in ghostStates: + ghostPos = ghost.getPosition() + ghostDist = manhattanDistance(pacPos, ghostPos) + + if ghost.scaredTimer > 0: + total += 33.7 / (ghostDist + 1.3) + else: + if ghostDist < 2: + total -= 426.5 + else: + total -= 4.6 / (ghostDist + 0.9) + + if foodList: + avgFoodDist = sum(manhattanDistance(pacPos, f) for f in foodList) / len(foodList) + total += 6.8 / (avgFoodDist + 2.0) + + return total + +# Abbreviation +better = betterEvaluationFunction diff --git a/searchAgents.py b/searchAgents.py new file mode 100644 index 0000000..9daab00 --- /dev/null +++ b/searchAgents.py @@ -0,0 +1,640 @@ +# searchAgents.py +# --------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + +#Authors: Ayesha Kazi, Mahina Khan + +""" +This file contains all of the agents that can be selected to control Pacman. To +select an agent, use the '-p' option when running pacman.py. Arguments can be +passed to your agent using '-a'. For example, to load a SearchAgent that uses +depth first search (dfs), run the following command: + +> python pacman.py -p SearchAgent -a fn=depthFirstSearch + +Commands to invoke other search strategies can be found in the project +description. + +Please only change the parts of the file you are asked to. Look for the lines +that say + +"*** YOUR CODE HERE ***" + +The parts you fill in start about 3/4 of the way down. Follow the project +description for details. + +Good luck and happy searching! +""" + +from typing import List, Tuple, Any +from game import Directions +from game import Agent +from game import Actions +from util import manhattanDistance +import util +import time +import search +import pacman + +class GoWestAgent(Agent): + "An agent that goes West until it can't." + + def getAction(self, state): + "The agent receives a GameState (defined in pacman.py)." + if Directions.WEST in state.getLegalPacmanActions(): + return Directions.WEST + else: + return Directions.STOP + +####################################################### +# This portion is written for you, but will only work # +# after you fill in parts of search.py # +####################################################### + +class SearchAgent(Agent): + """ + This very general search agent finds a path using a supplied search + algorithm for a supplied search problem, then returns actions to follow that + path. + + As a default, this agent runs DFS on a PositionSearchProblem to find + location (1,1) + + Options for fn include: + depthFirstSearch or dfs + breadthFirstSearch or bfs + + + Note: You should NOT change any code in SearchAgent + """ + + def __init__(self, fn='depthFirstSearch', prob='PositionSearchProblem', heuristic='nullHeuristic'): + # Warning: some advanced Python magic is employed below to find the right functions and problems + + # Get the search function from the name and heuristic + if fn not in dir(search): + raise AttributeError(fn + ' is not a search function in search.py.') + func = getattr(search, fn) + if 'heuristic' not in func.__code__.co_varnames: + print('[SearchAgent] using function ' + fn) + self.searchFunction = func + else: + if heuristic in globals().keys(): + heur = globals()[heuristic] + elif heuristic in dir(search): + heur = getattr(search, heuristic) + else: + raise AttributeError(heuristic + ' is not a function in searchAgents.py or search.py.') + print('[SearchAgent] using function %s and heuristic %s' % (fn, heuristic)) + # Note: this bit of Python trickery combines the search algorithm and the heuristic + self.searchFunction = lambda x: func(x, heuristic=heur) + + # Get the search problem type from the name + if prob not in globals().keys() or not prob.endswith('Problem'): + raise AttributeError(prob + ' is not a search problem type in SearchAgents.py.') + self.searchType = globals()[prob] + print('[SearchAgent] using problem type ' + prob) + + def registerInitialState(self, state): + """ + This is the first time that the agent sees the layout of the game + board. Here, we choose a path to the goal. In this phase, the agent + should compute the path to the goal and store it in a local variable. + All of the work is done in this method! + + state: a GameState object (pacman.py) + """ + if self.searchFunction == None: raise Exception("No search function provided for SearchAgent") + starttime = time.time() + problem = self.searchType(state) # Makes a new search problem + self.actions = self.searchFunction(problem) # Find a path + if self.actions == None: + self.actions = [] + totalCost = problem.getCostOfActions(self.actions) + print('Path found with total cost of %d in %.1f seconds' % (totalCost, time.time() - starttime)) + if '_expanded' in dir(problem): print('Search nodes expanded: %d' % problem._expanded) + + def getAction(self, state): + """ + Returns the next action in the path chosen earlier (in + registerInitialState). Return Directions.STOP if there is no further + action to take. + + state: a GameState object (pacman.py) + """ + if 'actionIndex' not in dir(self): self.actionIndex = 0 + i = self.actionIndex + self.actionIndex += 1 + if i < len(self.actions): + return self.actions[i] + else: + return Directions.STOP + +class PositionSearchProblem(search.SearchProblem): + """ + A search problem defines the state space, start state, goal test, successor + function and cost function. This search problem can be used to find paths + to a particular point on the pacman board. + + The state space consists of (x,y) positions in a pacman game. + + Note: this search problem is fully specified; you should NOT change it. + """ + + def __init__(self, gameState, costFn = lambda x: 1, goal=(1,1), start=None, warn=True, visualize=True): + """ + Stores the start and goal. + + gameState: A GameState object (pacman.py) + costFn: A function from a search state (tuple) to a non-negative number + goal: A position in the gameState + """ + self.walls = gameState.getWalls() + self.startState = gameState.getPacmanPosition() + if start != None: self.startState = start + self.goal = goal + self.costFn = costFn + self.visualize = visualize + if warn and (gameState.getNumFood() != 1 or not gameState.hasFood(*goal)): + print('Warning: this does not look like a regular search maze') + + # For display purposes + self._visited, self._visitedlist, self._expanded = {}, [], 0 # DO NOT CHANGE + + def getStartState(self): + return self.startState + + def isGoalState(self, state): + isGoal = state == self.goal + + # For display purposes only + if isGoal and self.visualize: + self._visitedlist.append(state) + import __main__ + if '_display' in dir(__main__): + if 'drawExpandedCells' in dir(__main__._display): #@UndefinedVariable + __main__._display.drawExpandedCells(self._visitedlist) #@UndefinedVariable + + return isGoal + + def getSuccessors(self, state): + """ + Returns successor states, the actions they require, and a cost of 1. + + As noted in search.py: + For a given state, this should return a list of triples, + (successor, action, stepCost), where 'successor' is a + successor to the current state, 'action' is the action + required to get there, and 'stepCost' is the incremental + cost of expanding to that successor + """ + + successors = [] + for action in [Directions.NORTH, Directions.SOUTH, Directions.EAST, Directions.WEST]: + x,y = state + dx, dy = Actions.directionToVector(action) + nextx, nexty = int(x + dx), int(y + dy) + if not self.walls[nextx][nexty]: + nextState = (nextx, nexty) + cost = self.costFn(nextState) + successors.append( ( nextState, action, cost) ) + + # Bookkeeping for display purposes + self._expanded += 1 # DO NOT CHANGE + if state not in self._visited: + self._visited[state] = True + self._visitedlist.append(state) + + return successors + + def getCostOfActions(self, actions): + """ + Returns the cost of a particular sequence of actions. If those actions + include an illegal move, return 999999. + """ + if actions == None: return 999999 + x,y= self.getStartState() + cost = 0 + for action in actions: + # Check figure out the next state and see whether its' legal + dx, dy = Actions.directionToVector(action) + x, y = int(x + dx), int(y + dy) + if self.walls[x][y]: return 999999 + cost += self.costFn((x,y)) + return cost + +class StayEastSearchAgent(SearchAgent): + """ + An agent for position search with a cost function that penalizes being in + positions on the West side of the board. + + The cost function for stepping into a position (x,y) is 1/2^x. + """ + def __init__(self): + self.searchFunction = search.uniformCostSearch + costFn = lambda pos: .5 ** pos[0] + self.searchType = lambda state: PositionSearchProblem(state, costFn, (1, 1), None, False) + +class StayWestSearchAgent(SearchAgent): + """ + An agent for position search with a cost function that penalizes being in + positions on the East side of the board. + + The cost function for stepping into a position (x,y) is 2^x. + """ + def __init__(self): + self.searchFunction = search.uniformCostSearch + costFn = lambda pos: 2 ** pos[0] + self.searchType = lambda state: PositionSearchProblem(state, costFn) + +def manhattanHeuristic(position, problem, info={}): + "The Manhattan distance heuristic for a PositionSearchProblem" + xy1 = position + xy2 = problem.goal + return abs(xy1[0] - xy2[0]) + abs(xy1[1] - xy2[1]) + +def euclideanHeuristic(position, problem, info={}): + "The Euclidean distance heuristic for a PositionSearchProblem" + xy1 = position + xy2 = problem.goal + return ( (xy1[0] - xy2[0]) ** 2 + (xy1[1] - xy2[1]) ** 2 ) ** 0.5 + +##################################################### +# This portion is incomplete. Time to write code! # +##################################################### + +class CornersProblem(search.SearchProblem): + """ + This search problem finds paths through all four corners of a layout. + + You must select a suitable state space and successor function + """ + + def __init__(self, startingGameState: pacman.GameState): + """ + Stores the walls, pacman's starting position and corners. + """ + self.walls = startingGameState.getWalls() + self.startingPosition = startingGameState.getPacmanPosition() + top, right = self.walls.height-2, self.walls.width-2 + self.corners = ((1,1), (1,top), (right, 1), (right, top)) + for corner in self.corners: + if not startingGameState.hasFood(*corner): + print('Warning: no food in corner ' + str(corner)) + self._expanded = 0 # DO NOT CHANGE; Number of search nodes expanded + + def getStartState(self): + """ + Returns the start state (in your state space, not the full Pacman state + space) + Return the initial search state. + State shape for CornersProblem: + (position: (x, y), visitedCorners: frozenset of corner coordinates ) + Mark the start corner as visited if Pacman starts on one. + """ + "*** YOUR CODE HERE ***" + visited = frozenset() + if self.startingPosition in self.corners: + visited = frozenset([self.startingPosition]) + start_state = (self.startingPosition, visited) + return start_state + + def isGoalState(self, state: Any): + """ + Returns whether this search state is a goal state of the problem. + Goal: all four corners have been visited. + Read the visited set from the given state + """ + "*** YOUR CODE HERE ***" + (position, visited) = state + return len(visited) == len(self.corners) + + def getSuccessors(self, state: Any): + """ + Returns successor states, the actions they require, and a cost of 1. + + As noted in search.py: + For a given state, this should return a list of triples, (successor, + action, stepCost), where 'successor' is a successor to the current + state, 'action' is the action required to get there, and 'stepCost' + is the incremental cost of expanding to that successor + + Generate all legal successors from `state`. + Skip actions that go out of bounds or into a wall. + Update the visited-corners info when at a corner. + """ + + successors = [] + (position, visited) = state + W, H = self.walls.width, self.walls.height + for action in [Directions.NORTH, Directions.SOUTH, Directions.EAST, Directions.WEST]: + # Add a successor state to the successor list if the action is legal + # Here's a code snippet for figuring out whether a new position hits a wall: + # x,y = currentPosition + # dx, dy = Actions.directionToVector(action) + # nextx, nexty = int(x + dx), int(y + dy) + # hitsWall = self.walls[nextx][nexty] + + "*** YOUR CODE HERE ***" + x,y = position + dx, dy = Actions.directionToVector(action) + nextx, nexty = int(x + dx), int(y + dy) + + if not (0 <= nextx < W and 0 <= nexty < H): + continue + + # Hits wall? + if self.walls[nextx][nexty]: + continue + + # New position and updated corner-visit info + next_pos = (nextx, nexty) + next_visited = visited + if next_pos in self.corners: + next_visited = visited | frozenset([next_pos]) + successor_state = (next_pos, next_visited) + successors.append((successor_state, action, 1)) + + self._expanded += 1 # DO NOT CHANGE + return successors + + def getCostOfActions(self, actions): + """ + Returns the cost of a particular sequence of actions. If those actions + include an illegal move, return 999999. This is implemented for you. + """ + if actions == None: return 999999 + x,y= self.startingPosition + for action in actions: + dx, dy = Actions.directionToVector(action) + x, y = int(x + dx), int(y + dy) + if self.walls[x][y]: return 999999 + return len(actions) + + +def cornersHeuristic(state: Any, problem: CornersProblem): + """ + A heuristic for the CornersProblem that you defined. + + state: The current search state + (a data structure you chose in your search problem) + + problem: The CornersProblem instance for this layout. + + This function should always return a number that is a lower bound on the + shortest path from the state to a goal of the problem; i.e. it should be + admissible. + + h(state) = max over remaining corners of Manhattan(current_pos, corner) + At least reach the farthest unvisited corner. + Manhattan is a good lower bound + """ + corners = problem.corners # These are the corner coordinates + walls = problem.walls # These are the walls of the maze, as a Grid (game.py) + + "*** YOUR CODE HERE ***" + (position, visited) = state + remaining = [c for c in problem.corners if c not in visited] + if not remaining: + return 0 + # Return the farthest corner's Manhattan distance from the current position + return max(manhattanDistance(position, c) for c in remaining) + + + +class AStarCornersAgent(SearchAgent): + "A SearchAgent for FoodSearchProblem using A* and your foodHeuristic" + def __init__(self): + self.searchFunction = lambda prob: search.aStarSearch(prob, cornersHeuristic) + self.searchType = CornersProblem + +class FoodSearchProblem: + """ + A search problem associated with finding the a path that collects all of the + food (dots) in a Pacman game. + + A search state in this problem is a tuple ( pacmanPosition, foodGrid ) where + pacmanPosition: a tuple (x,y) of integers specifying Pacman's position + foodGrid: a Grid (see game.py) of either True or False, specifying remaining food + """ + def __init__(self, startingGameState: pacman.GameState): + self.start = (startingGameState.getPacmanPosition(), startingGameState.getFood()) + self.walls = startingGameState.getWalls() + self.startingGameState = startingGameState + self._expanded = 0 # DO NOT CHANGE + self.heuristicInfo = {} # A dictionary for the heuristic to store information + + def getStartState(self): + return self.start + + def isGoalState(self, state): + return state[1].count() == 0 + + def getSuccessors(self, state): + "Returns successor states, the actions they require, and a cost of 1." + successors = [] + self._expanded += 1 # DO NOT CHANGE + for direction in [Directions.NORTH, Directions.SOUTH, Directions.EAST, Directions.WEST]: + x,y = state[0] + dx, dy = Actions.directionToVector(direction) + nextx, nexty = int(x + dx), int(y + dy) + if not self.walls[nextx][nexty]: + nextFood = state[1].copy() + nextFood[nextx][nexty] = False + successors.append( ( ((nextx, nexty), nextFood), direction, 1) ) + return successors + + def getCostOfActions(self, actions): + """Returns the cost of a particular sequence of actions. If those actions + include an illegal move, return 999999""" + x,y= self.getStartState()[0] + cost = 0 + for action in actions: + # figure out the next state and see whether it's legal + dx, dy = Actions.directionToVector(action) + x, y = int(x + dx), int(y + dy) + if self.walls[x][y]: + return 999999 + cost += 1 + return cost + +class AStarFoodSearchAgent(SearchAgent): + "A SearchAgent for FoodSearchProblem using A* and your foodHeuristic" + def __init__(self): + self.searchFunction = lambda prob: search.aStarSearch(prob, foodHeuristic) + self.searchType = FoodSearchProblem + +def foodHeuristic(state: Tuple[Tuple, List[List]], problem: FoodSearchProblem): + """ + Your heuristic for the FoodSearchProblem goes here. + + If using A* ever finds a solution that is worse uniform cost search finds, + your search may have a but our your heuristic is not admissible! On the + other hand, inadmissible heuristics may find optimal solutions, so be careful. + + The state is a tuple ( pacmanPosition, foodGrid ) where foodGrid is a Grid + (see game.py) of either True or False. You can call foodGrid.asList() to get + a list of food coordinates instead. + + If you want access to info like walls, capsules, etc., you can query the + problem. For example, problem.walls gives you a Grid of where the walls + are. + + If you want to *store* information to be reused in other calls to the + heuristic, there is a dictionary called problem.heuristicInfo that you can + use. For example, if you only want to count the walls once and store that + value, try: problem.heuristicInfo['wallCount'] = problem.walls.count() + Subsequent calls to this heuristic can access + problem.heuristicInfo['wallCount'] + + h = (distance from Pacman to nearest remaining food) + + (MST weight over remaining foods), + using Manhattan distances for edges. Cache pairwise Manhattan distances + and MST weights in problem.heuristicInfo to avoid computing again. + """ + position, foodGrid = state + "*** YOUR CODE HERE ***" + foods = foodGrid.asList() # list of food coordinates + if not foods: + return 0 # no food left is the goal + + # Caching to avoid doing the same computation over and over again + info = problem.heuristicInfo + md_cache = info.setdefault('md_cache', {}) + mst_cache = info.setdefault('mst_cache', {}) + + # Fast Manhattan with caching + def md(a, b): + key = (a, b) if a <= b else (b, a) + if key not in md_cache: + md_cache[key] = abs(a[0] - b[0]) + abs(a[1] - b[1]) + return md_cache[key] + # Compute MST weight of the remaining foods (Prim's algorithm) + fset = frozenset(foods) + if fset in mst_cache: + mst_weight = mst_cache[fset] + else: + if len(foods) <= 1: + mst_weight = 0 + else: + # Build an MST over foods with Manhattan distances as edge weights. + tree = {foods[0]} + remaining = set(foods[1:]) + mst_weight = 0 + while remaining: + best_d = None + best_v = None + # Pick the node with minimal distance to the tree + for v in remaining: + d = min(md(v, u) for u in tree) + if best_d is None or d < best_d: + best_d, best_v = d, v + mst_weight += best_d + tree.add(best_v) + remaining.remove(best_v) + mst_cache[fset] = mst_weight # cache by the set of remaining foods + + entry = min(md(position, f) for f in foods) + # Get to some food and then connect the foods + return entry + mst_weight + + +class ClosestDotSearchAgent(SearchAgent): + "Search for all food using a sequence of searches" + def registerInitialState(self, state): + self.actions = [] + currentState = state + while(currentState.getFood().count() > 0): + nextPathSegment = self.findPathToClosestDot(currentState) # The missing piece + self.actions += nextPathSegment + for action in nextPathSegment: + legal = currentState.getLegalActions() + if action not in legal: + t = (str(action), str(currentState)) + raise Exception('findPathToClosestDot returned an illegal move: %s!\n%s' % t) + currentState = currentState.generateSuccessor(0, action) + self.actionIndex = 0 + print('Path found with cost %d.' % len(self.actions)) + + def findPathToClosestDot(self, gameState: pacman.GameState): + """ + Returns a path (a list of actions) to the closest dot, starting from + gameState. + """ + # Here are some useful elements of the startState + startPosition = gameState.getPacmanPosition() + food = gameState.getFood() + walls = gameState.getWalls() + problem = AnyFoodSearchProblem(gameState) + + "*** YOUR CODE HERE ***" + problem = AnyFoodSearchProblem(gameState) + if problem.isGoalState(problem.getStartState()): + return [] + # BFS returns a shortest path in steps + return search.bfs(problem) + + +class AnyFoodSearchProblem(PositionSearchProblem): + """ + A search problem for finding a path to any food. + + This search problem is just like the PositionSearchProblem, but has a + different goal test, which you need to fill in below. The state space and + successor function do not need to be changed. + + The class definition above, AnyFoodSearchProblem(PositionSearchProblem), + inherits the methods of the PositionSearchProblem. + + You can use this search problem to help you fill in the findPathToClosestDot + method. + """ + + def __init__(self, gameState): + "Stores information from the gameState. You don't need to change this." + # Store the food for later reference + self.food = gameState.getFood() + + # Store info for the PositionSearchProblem (no need to change this) + self.walls = gameState.getWalls() + self.startState = gameState.getPacmanPosition() + self.costFn = lambda x: 1 + self._visited, self._visitedlist, self._expanded = {}, [], 0 # DO NOT CHANGE + + def isGoalState(self, state: Tuple[int, int]): + """ + The state is Pacman's position. Fill this in with a goal test that will + complete the problem definition. + Goal Test: return True iff this position currently has food. + """ + x,y = state + + "*** YOUR CODE HERE ***" + return self.food[x][y] + +def mazeDistance(point1: Tuple[int, int], point2: Tuple[int, int], gameState: pacman.GameState) -> int: + """ + Returns the maze distance between any two points, using the search functions + you have already built. The gameState can be any game state -- Pacman's + position in that state is ignored. + + Example usage: mazeDistance( (2,4), (5,6), gameState) + + This might be a useful helper function for your ApproximateSearchAgent. + """ + x1, y1 = point1 + x2, y2 = point2 + walls = gameState.getWalls() + assert not walls[x1][y1], 'point1 is a wall: ' + str(point1) + assert not walls[x2][y2], 'point2 is a wall: ' + str(point2) + prob = PositionSearchProblem(gameState, start=point1, goal=point2, warn=False, visualize=False) + return len(search.bfs(prob))