# 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 score == 0 and myState.isPacman and carrying > 0:
if carrying >= 2 or homeDist <= 9 or ghostDist <= homeDist + 2:
return self.closestPortal(myPos), 'return'
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)
score = self.getScore(gameState)
bestFood = None
bestCost = float('inf')
for food in foodList:
goDist = self.getMazeDistance(myPos, food)
homeDist = self.minDistance(food, self.portals)
homeWeight = 1.15 if score == 0 else 0.65
deadEndWeight = 5.0 if score == 0 else 2.5
cost = goDist + homeWeight * homeDist
cost += self.deadEndDepth.get(food, 0) * deadEndWeight
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 5
if score <= 0:
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)))