Source: risk_courier_portal_20260430_045048

RawBack
# 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)))