# myTeam.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). from collections import deque from captureAgents import CaptureAgent from game import Directions from util import nearestPoint def createTeam(firstIndex, secondIndex, isRed, first='HybridCourierAgent', second='HybridCourierAgent'): return [ HybridCourierAgent(firstIndex, baseRole='offense'), HybridCourierAgent(secondIndex, baseRole='defense'), ] class HybridCourierAgent(CaptureAgent): """Risk-aware courier with portal defense and controlled role switching.""" ACTION_PRIORITY = [ Directions.NORTH, Directions.SOUTH, Directions.EAST, Directions.WEST, Directions.STOP, ] def __init__(self, index, baseRole='offense'): CaptureAgent.__init__(self, index) self.baseRole = baseRole self.start = None self.width = 0 self.height = 0 self.portals = [] self.deadEndDepth = {} self.recentPositions = deque(maxlen=8) self.lastEatenFood = None self.patrolIndex = 0 def registerInitialState(self, gameState): CaptureAgent.registerInitialState(self, gameState) self.start = gameState.getAgentPosition(self.index) walls = gameState.getWalls() self.width = walls.width self.height = walls.height self.portals = self.computePortals(gameState) or [self.start] self.deadEndDepth = self.computeDeadEndDepths(gameState) self.patrolIndex = self.index % max(1, len(self.portals)) def chooseAction(self, gameState): actions = gameState.getLegalActions(self.index) if len(actions) > 1 and Directions.STOP in actions: actions.remove(Directions.STOP) currentPos = gameState.getAgentPosition(self.index) if currentPos is not None: self.recentPositions.append(currentPos) self.updateLastEatenFood(gameState) target, mode = self.selectTarget(gameState) scored = [] for action in actions: successor = self.getSuccessor(gameState, action) myState = successor.getAgentState(self.index) myPos = nearestPoint(myState.getPosition()) value = self.evaluateAction(gameState, successor, action, myPos, myState, target, mode) scored.append((value, action)) bestValue = max(value for value, action in scored) bestActions = [action for value, action in scored if value == bestValue] return self.breakTies(bestActions) def getSuccessor(self, gameState, action): successor = gameState.generateSuccessor(self.index, action) pos = successor.getAgentState(self.index).getPosition() if pos != nearestPoint(pos): return successor.generateSuccessor(self.index, action) return successor def selectTarget(self, gameState): myState = gameState.getAgentState(self.index) myPos = gameState.getAgentPosition(self.index) score = self.getScore(gameState) timeLeft = getattr(gameState.data, 'timeleft', 1200) invaders = self.getInvaders(gameState) if invaders: maxCarry = max(invader.numCarrying for invader in invaders) if len(invaders) >= 2 or maxCarry >= 3 or self.baseRole == 'defense': return self.interceptTarget(gameState, invaders), 'defense' if timeLeft < 140 and score > 0: if self.baseRole == 'defense' or invaders: return self.defenseTarget(gameState, invaders), 'patrol' if myState.numCarrying > 0: return self.closestPortal(myPos), 'return' if self.baseRole == 'defense' and not (score <= 0 and timeLeft > 220 and not invaders): return self.defenseTarget(gameState, invaders), 'patrol' food = self.getFood(gameState).asList() if len(food) <= 2: return self.closestPortal(myPos), 'return' carrying = myState.numCarrying homeDist = self.minDistance(myPos, self.portals) ghostDist = self.activeGhostDistance(gameState, myPos) threshold = self.carryThreshold(score, timeLeft) if myState.isPacman and carrying > 0: if carrying >= threshold: return self.closestPortal(myPos), 'return' if ghostDist <= homeDist + 1: return self.closestPortal(myPos), 'return' if timeLeft < 4 * homeDist + 30: return self.closestPortal(myPos), 'return' capsules = self.getCapsules(gameState) if capsules and ghostDist <= 6: nearestCapsule = min(capsules, key=lambda cap: self.getMazeDistance(myPos, cap)) if self.getMazeDistance(myPos, nearestCapsule) <= ghostDist + 2: return nearestCapsule, 'capsule' return self.bestFoodTarget(gameState, myPos, food), 'offense' def evaluateAction(self, oldState, successor, action, myPos, myState, target, mode): value = 0.0 value += 10000.0 * self.getScore(successor) if target is not None: value -= 20.0 * self.getMazeDistance(myPos, target) value -= 45.0 * len(self.getFood(successor).asList()) value += 12.0 * myState.numCarrying if mode in ('return', 'capsule'): value -= 7.0 * self.minDistance(myPos, self.portals) if myState.isPacman: ghostDist = self.activeGhostDistance(successor, myPos) homeDist = self.minDistance(myPos, self.portals) value -= myState.numCarrying * homeDist * 6.0 value -= self.deadEndDepth.get(myPos, 0) * max(0, 5 - ghostDist) * 18.0 if ghostDist <= 1: value -= 2400.0 elif ghostDist <= 3: value -= 700.0 / (ghostDist + 0.1) elif ghostDist <= 5: value -= 80.0 / ghostDist if mode in ('defense', 'patrol'): if myState.isPacman: value -= 2200.0 invaders = self.getInvaders(successor) if invaders and myState.scaredTimer == 0: invaderDist = min( self.getMazeDistance(myPos, nearestPoint(invader.getPosition())) for invader in invaders ) value -= 130.0 * invaderDist value -= 500.0 * len(invaders) elif target is not None: value -= 5.0 * self.getMazeDistance(myPos, target) if action == Directions.STOP: value -= 120.0 reverse = Directions.REVERSE[ oldState.getAgentState(self.index).configuration.direction ] if action == reverse: value -= 4.0 if myPos in self.recentPositions: value -= 16.0 return value def bestFoodTarget(self, gameState, myPos, foodList): teammatePositions = [] for teammate in self.getTeam(gameState): if teammate != self.index: pos = gameState.getAgentPosition(teammate) if pos is not None: teammatePositions.append(pos) activeGhosts = self.activeEnemyGhosts(gameState) bestFood = None bestCost = float('inf') for food in foodList: goDist = self.getMazeDistance(myPos, food) homeDist = self.minDistance(food, self.portals) cost = goDist + 0.65 * homeDist cost += self.deadEndDepth.get(food, 0) * 2.5 cost -= self.foodClusterBonus(food, foodList) for ghost in activeGhosts: ghostPos = nearestPoint(ghost.getPosition()) ghostToFood = self.getMazeDistance(ghostPos, food) if ghostToFood <= goDist + 2: cost += 35.0 + 12.0 * (goDist + 3 - ghostToFood) cost += self.deadEndDepth.get(food, 0) * 8.0 for teammatePos in teammatePositions: if self.getMazeDistance(teammatePos, food) <= 3: cost += 7.0 if cost < bestCost: bestCost = cost bestFood = food return bestFood def defenseTarget(self, gameState, invaders): if invaders: return self.interceptTarget(gameState, invaders) if self.lastEatenFood is not None: return self.closestPortal(self.lastEatenFood) defendedFood = self.getFoodYouAreDefending(gameState).asList() if defendedFood: return min(self.portals, key=lambda portal: self.minDistance(portal, defendedFood)) return self.portals[self.patrolIndex % len(self.portals)] def interceptTarget(self, gameState, invaders): myPos = gameState.getAgentPosition(self.index) targetInvader = max( invaders, key=lambda invader: ( invader.numCarrying, -self.getMazeDistance(myPos, nearestPoint(invader.getPosition())) ) ) invaderPos = nearestPoint(targetInvader.getPosition()) if targetInvader.numCarrying >= 2: return invaderPos return self.closestPortal(invaderPos) def computePortals(self, gameState): walls = gameState.getWalls() homeX = self.width // 2 - 1 if self.red else self.width // 2 enemyX = self.width // 2 if self.red else self.width // 2 - 1 portals = [] for y in range(1, self.height - 1): if not walls[homeX][y] and not walls[enemyX][y]: portals.append((homeX, y)) return portals def computeDeadEndDepths(self, gameState): walls = gameState.getWalls() openCells = [] degree = {} for x in range(1, self.width - 1): for y in range(1, self.height - 1): if not walls[x][y]: pos = (x, y) openCells.append(pos) degree[pos] = len(self.openNeighbors(pos, walls)) depths = dict((pos, 0) for pos in openCells) queue = deque((pos, 1) for pos in openCells if degree[pos] <= 1) removed = set() while queue: pos, depth = queue.popleft() if pos in removed: continue removed.add(pos) depths[pos] = depth for neighbor in self.openNeighbors(pos, walls): if neighbor in removed: continue degree[neighbor] -= 1 if degree[neighbor] <= 1: queue.append((neighbor, depth + 1)) return depths def openNeighbors(self, pos, walls): x, y = pos neighbors = [] for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): nx, ny = x + dx, y + dy if 0 <= nx < self.width and 0 <= ny < self.height and not walls[nx][ny]: neighbors.append((nx, ny)) return neighbors def updateLastEatenFood(self, gameState): previous = self.getPreviousObservation() if previous is None: return oldFood = set(self.getFoodYouAreDefending(previous).asList()) newFood = set(self.getFoodYouAreDefending(gameState).asList()) eaten = oldFood - newFood if eaten: myPos = gameState.getAgentPosition(self.index) self.lastEatenFood = min(eaten, key=lambda pos: self.getMazeDistance(myPos, pos)) def getInvaders(self, gameState): invaders = [] for opponent in self.getOpponents(gameState): enemy = gameState.getAgentState(opponent) if enemy.getPosition() is not None and enemy.isPacman: invaders.append(enemy) return invaders def activeEnemyGhosts(self, gameState): ghosts = [] for opponent in self.getOpponents(gameState): enemy = gameState.getAgentState(opponent) if enemy.getPosition() is not None and not enemy.isPacman: if enemy.scaredTimer <= 1: ghosts.append(enemy) return ghosts def activeGhostDistance(self, gameState, pos): if pos is None: return 999 ghosts = self.activeEnemyGhosts(gameState) if not ghosts: return 999 return min(self.getMazeDistance(pos, nearestPoint(ghost.getPosition())) for ghost in ghosts) def minDistance(self, pos, targets): if pos is None or not targets: return 999 pos = nearestPoint(pos) return min(self.getMazeDistance(pos, nearestPoint(target)) for target in targets) def closestPortal(self, pos): if pos is None: return self.portals[self.patrolIndex % len(self.portals)] pos = nearestPoint(pos) return min(self.portals, key=lambda portal: self.getMazeDistance(pos, portal)) def carryThreshold(self, score, timeLeft): if timeLeft < 180: return 1 if score >= 0 else 3 if score >= 5: return 2 if score < -5: return 4 return 3 def foodClusterBonus(self, food, foodList): fx, fy = food nearby = 0 for other in foodList: if other == food: continue ox, oy = other if abs(fx - ox) + abs(fy - oy) <= 3: nearby += 1 return min(nearby, 4) * 2.0 def breakTies(self, actions): order = dict((action, rank) for rank, action in enumerate(self.ACTION_PRIORITY)) return min(actions, key=lambda action: order.get(action, len(order)))