Source: portal_chokepoint_defense_20260429_192100

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 captureAgents import CaptureAgent
import random
import util
from game import Directions
from util import nearestPoint


def createTeam(firstIndex, secondIndex, isRed,
               first='FoodReturnOffensiveAgent',
               second='PortalChokepointDefender'):
  return [eval(first)(firstIndex), eval(second)(secondIndex)]


class ReflexCaptureAgent(CaptureAgent):
  def registerInitialState(self, gameState):
    self.start = gameState.getAgentPosition(self.index)
    CaptureAgent.registerInitialState(self, gameState)
    self.width = gameState.data.layout.width
    self.height = gameState.data.layout.height
    self.portals = self.computePortals(gameState)

  def chooseAction(self, gameState):
    actions = gameState.getLegalActions(self.index)
    values = [self.evaluate(gameState, action) for action in actions]
    bestValue = max(values)
    bestActions = [action for action, value in zip(actions, values)
                   if value == bestValue]
    if Directions.STOP in bestActions and len(bestActions) > 1:
      bestActions.remove(Directions.STOP)
    return random.choice(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 evaluate(self, gameState, action):
    return self.getFeatures(gameState, action) * self.getWeights(gameState, action)

  def getFeatures(self, gameState, action):
    features = util.Counter()
    successor = self.getSuccessor(gameState, action)
    features['successorScore'] = self.getScore(successor)
    return features

  def getWeights(self, gameState, action):
    return {'successorScore': 1.0}

  def computePortals(self, gameState):
    walls = gameState.getWalls()
    if self.red:
      homeX = self.width // 2 - 1
      enemyX = self.width // 2
    else:
      homeX = self.width // 2
      enemyX = self.width // 2 - 1

    portals = []
    fallback = []
    for y in range(1, self.height - 1):
      if not walls[homeX][y]:
        fallback.append((homeX, y))
        if not walls[enemyX][y]:
          portals.append((homeX, y))
    return portals or fallback or [self.start]

  def isHomeSide(self, pos):
    if pos is None:
      return True
    if self.red:
      return pos[0] < self.width // 2
    return pos[0] >= self.width // 2

  def minDistance(self, pos, targets):
    if pos is None or not targets:
      return 0
    return min(self.getMazeDistance(pos, target) for target in targets)

  def activeEnemyGhostDistances(self, gameState, myPos):
    distances = []
    for opponent in self.getOpponents(gameState):
      enemy = gameState.getAgentState(opponent)
      enemyPos = enemy.getPosition()
      if enemyPos is None:
        continue
      if not enemy.isPacman and enemy.scaredTimer <= 1:
        distances.append(self.getMazeDistance(myPos, enemyPos))
    return distances


class FoodReturnOffensiveAgent(ReflexCaptureAgent):
  def getFeatures(self, gameState, action):
    features = util.Counter()
    successor = self.getSuccessor(gameState, action)
    myState = successor.getAgentState(self.index)
    myPos = myState.getPosition()
    foodList = self.getFood(successor).asList()
    capsules = self.getCapsules(successor)
    ghostDistances = self.activeEnemyGhostDistances(successor, myPos)
    closestGhost = min(ghostDistances) if ghostDistances else None

    features['successorScore'] = self.getScore(successor)
    features['foodRemaining'] = len(foodList)
    features['distanceToFood'] = self.minDistance(myPos, foodList)
    features['distanceHome'] = self.minDistance(myPos, self.portals)
    features['distanceToCapsule'] = self.minDistance(myPos, capsules)

    if action == Directions.STOP:
      features['stop'] = 1
    reverse = Directions.REVERSE[gameState.getAgentState(self.index).configuration.direction]
    if action == reverse:
      features['reverse'] = 1

    if closestGhost is not None:
      features['ghostDistance'] = closestGhost
      if closestGhost <= 2:
        features['immediateDanger'] = 1
      elif closestGhost <= 5:
        features['nearGhost'] = 1

    carrying = myState.numCarrying
    if carrying >= 4 or len(foodList) <= 2:
      features['shouldReturn'] = 1
    if carrying > 0 and closestGhost is not None and closestGhost <= 5:
      features['shouldReturn'] = 1
    if gameState.data.timeleft < features['distanceHome'] + 20:
      features['shouldReturn'] = 1

    return features

  def getWeights(self, gameState, action):
    features = self.getFeatures(gameState, action)
    weights = {
      'successorScore': 200,
      'foodRemaining': -100,
      'distanceToFood': -4,
      'distanceToCapsule': -2,
      'ghostDistance': 2,
      'immediateDanger': -1000,
      'nearGhost': -200,
      'distanceHome': 0,
      'shouldReturn': 0,
      'stop': -100,
      'reverse': -3,
    }
    if features['shouldReturn']:
      weights['distanceHome'] = -16
      weights['distanceToFood'] = -1
    if features['nearGhost'] or features['immediateDanger']:
      weights['distanceToCapsule'] = -8
    return weights


class PortalChokepointDefender(ReflexCaptureAgent):
  def registerInitialState(self, gameState):
    ReflexCaptureAgent.registerInitialState(self, gameState)
    self.currentTarget = self.chooseFoodClusterPortal(gameState)
    self.lastMissingFood = None

  def chooseAction(self, gameState):
    self.updateMissingFood(gameState)
    self.currentTarget = self.chooseDefensiveTarget(gameState)
    return ReflexCaptureAgent.chooseAction(self, gameState)

  def getFeatures(self, gameState, action):
    features = util.Counter()
    successor = self.getSuccessor(gameState, action)
    myState = successor.getAgentState(self.index)
    myPos = myState.getPosition()
    invaders = self.visibleInvaders(successor)

    features['onDefense'] = 1
    if myState.isPacman:
      features['onDefense'] = 0
      features['outsideHome'] = 1
    elif not self.isHomeSide(myPos):
      features['outsideHome'] = 1

    features['numInvaders'] = len(invaders)
    if invaders:
      invaderPositions = [invader.getPosition() for invader in invaders]
      closestInvaderDistance = min(
        self.getMazeDistance(myPos, invaderPos)
        for invaderPos in invaderPositions
      )
      if self.currentTarget in invaderPositions:
        features['invaderDistance'] = closestInvaderDistance
      if myState.scaredTimer > 0 and closestInvaderDistance <= 2:
        features['scaredTooClose'] = 1
    elif self.currentTarget is not None:
      features['distanceToTarget'] = self.getMazeDistance(myPos, self.currentTarget)

    if invaders and self.currentTarget is not None:
      features['distanceToTarget'] = self.getMazeDistance(myPos, self.currentTarget)

    if action == Directions.STOP:
      features['stop'] = 1
    reverse = Directions.REVERSE[gameState.getAgentState(self.index).configuration.direction]
    if action == reverse:
      features['reverse'] = 1
    return features

  def getWeights(self, gameState, action):
    weights = {
      'numInvaders': -1000,
      'onDefense': 180,
      'outsideHome': -1200,
      'invaderDistance': -28,
      'distanceToTarget': -7,
      'scaredTooClose': -500,
      'stop': -120,
      'reverse': -2,
    }
    if gameState.getAgentState(self.index).scaredTimer > 0:
      weights['invaderDistance'] = 8
      weights['distanceToTarget'] = -9
    return weights

  def visibleInvaders(self, gameState):
    enemies = [gameState.getAgentState(i) for i in self.getOpponents(gameState)]
    return [enemy for enemy in enemies
            if enemy.isPacman and enemy.getPosition() is not None]

  def updateMissingFood(self, gameState):
    previous = self.getPreviousObservation()
    if previous is None:
      return
    previousFood = set(self.getFoodYouAreDefending(previous).asList())
    currentFood = set(self.getFoodYouAreDefending(gameState).asList())
    missing = list(previousFood - currentFood)
    if missing:
      myPos = gameState.getAgentPosition(self.index)
      self.lastMissingFood = min(
        missing,
        key=lambda food: self.getMazeDistance(myPos, food)
      )

  def chooseDefensiveTarget(self, gameState):
    myPos = gameState.getAgentPosition(self.index)
    invaders = self.visibleInvaders(gameState)
    if invaders:
      return self.chooseInvaderTarget(gameState, invaders, myPos)

    if self.lastMissingFood is not None:
      if myPos is not None and self.getMazeDistance(myPos, self.lastMissingFood) <= 1:
        self.lastMissingFood = None
      else:
        return self.lastMissingFood

    return self.choosePatrolTarget(gameState)

  def chooseInvaderTarget(self, gameState, invaders, myPos):
    myState = gameState.getAgentState(self.index)
    bestTarget = None
    bestScore = None

    for invader in invaders:
      invaderPos = invader.getPosition()
      nearestPortal = min(
        self.portals,
        key=lambda portal: self.getMazeDistance(invaderPos, portal)
      )
      myToInvader = self.getMazeDistance(myPos, invaderPos)
      myToPortal = self.getMazeDistance(myPos, nearestPortal)
      invaderToPortal = self.getMazeDistance(invaderPos, nearestPortal)
      canCoverExit = myToPortal <= invaderToPortal
      canCatchBeforeExit = myToInvader <= invaderToPortal + 1

      if myState.scaredTimer > 0:
        target = nearestPortal
        score = myToPortal
      elif canCoverExit and canCatchBeforeExit:
        target = invaderPos
        score = myToInvader
      else:
        target = nearestPortal
        lateToExit = max(0, myToPortal - invaderToPortal)
        score = 4 * lateToExit + myToPortal + 0.25 * invaderToPortal

      if bestScore is None or score < bestScore:
        bestTarget = target
        bestScore = score

    return bestTarget

  def chooseFoodClusterPortal(self, gameState):
    return self.sortedFoodClusterPortals(gameState)[0]

  def choosePatrolTarget(self, gameState):
    return self.sortedFoodClusterPortals(gameState)[0]

  def sortedFoodClusterPortals(self, gameState):
    defendingFood = self.getFoodYouAreDefending(gameState).asList()
    if not defendingFood:
      return sorted(self.portals, key=lambda portal: self.getMazeDistance(self.start, portal))

    clusterSize = max(1, min(8, len(defendingFood) // 2))

    def portalFoodScore(portal):
      foodDistances = sorted(
        self.getMazeDistance(portal, food)
        for food in defendingFood
      )
      clusterDistance = sum(foodDistances[:clusterSize]) / float(clusterSize)
      startDistance = self.getMazeDistance(self.start, portal)
      return (clusterDistance, startDistance)

    return sorted(
      self.portals,
      key=portalFoodScore
    )