Source: portal_planner_defense_20260430_123030

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


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


class PlanningCaptureAgent(CaptureAgent):
  actionOrder = [
    Directions.NORTH,
    Directions.SOUTH,
    Directions.EAST,
    Directions.WEST,
  ]
  vectors = {
    Directions.NORTH: (0, 1),
    Directions.SOUTH: (0, -1),
    Directions.EAST: (1, 0),
    Directions.WEST: (-1, 0),
  }

  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.walls = gameState.getWalls()
    self.portals = self.computePortals(gameState)

  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 cleanPosition(self, pos):
    if pos is None:
      return None
    return (int(pos[0]), int(pos[1]))

  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 nextPosition(self, pos, action):
    dx, dy = self.vectors[action]
    return (int(pos[0] + dx), int(pos[1] + dy))

  def isLegalPosition(self, pos):
    x, y = pos
    return 0 <= x < self.width and 0 <= y < self.height and not self.walls[x][y]

  def orderedLegalActions(self, gameState):
    legal = [action for action in gameState.getLegalActions(self.index)
             if action != Directions.STOP]
    current = gameState.getAgentState(self.index).configuration.direction
    reverse = Directions.REVERSE[current]

    ordered = []
    if current in legal:
      ordered.append(current)
    for action in self.actionOrder:
      if action in legal and action != current and action != reverse:
        ordered.append(action)
    if reverse in legal:
      ordered.append(reverse)
    return ordered or gameState.getLegalActions(self.index)

  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 activeEnemyGhostPositions(self, gameState):
    positions = []
    for opponent in self.getOpponents(gameState):
      enemy = gameState.getAgentState(opponent)
      pos = gameState.getAgentPosition(opponent)
      if pos is None:
        continue
      if not enemy.isPacman and enemy.scaredTimer <= 1:
        positions.append(pos)
    return positions

  def visibleInvaderPositions(self, gameState):
    positions = []
    for opponent in self.getOpponents(gameState):
      enemy = gameState.getAgentState(opponent)
      pos = gameState.getAgentPosition(opponent)
      if enemy.isPacman and pos is not None:
        positions.append(pos)
    return positions

  def dangerZones(self, gameState, radius):
    zones = set()
    for ghost in self.activeEnemyGhostPositions(gameState):
      queue = deque([(ghost, 0)])
      seen = set([ghost])
      while queue:
        pos, depth = queue.popleft()
        zones.add(pos)
        if depth >= radius:
          continue
        for action in self.actionOrder:
          nxt = self.nextPosition(pos, action)
          if nxt in seen or not self.isLegalPosition(nxt):
            continue
          seen.add(nxt)
          queue.append((nxt, depth + 1))
    return zones

  def choosePathAction(self, gameState, targets, avoid=None, allowStop=False):
    start = self.cleanPosition(gameState.getAgentPosition(self.index))
    targetSet = set(self.cleanPosition(target) for target in targets if target is not None)
    avoid = avoid or set()

    if start is None or not targetSet:
      return self.safeFallbackAction(gameState, targetSet, avoid)
    if start in targetSet and allowStop:
      return Directions.STOP

    queue = deque()
    seen = set([start])
    legal = self.orderedLegalActions(gameState)
    for action in legal:
      if action == Directions.STOP:
        continue
      nxt = self.nextPosition(start, action)
      if not self.isLegalPosition(nxt) or nxt in seen or nxt in avoid:
        continue
      if nxt in targetSet:
        return action
      seen.add(nxt)
      queue.append((nxt, action))

    while queue:
      pos, firstAction = queue.popleft()
      for action in self.actionOrder:
        nxt = self.nextPosition(pos, action)
        if not self.isLegalPosition(nxt) or nxt in seen or nxt in avoid:
          continue
        if nxt in targetSet:
          return firstAction
        seen.add(nxt)
        queue.append((nxt, firstAction))

    return self.safeFallbackAction(gameState, targetSet, avoid)

  def safeFallbackAction(self, gameState, targets, avoid):
    myPos = self.cleanPosition(gameState.getAgentPosition(self.index))
    ghosts = self.activeEnemyGhostPositions(gameState)
    reverse = Directions.REVERSE[gameState.getAgentState(self.index).configuration.direction]
    bestAction = Directions.STOP
    bestScore = None

    for action in gameState.getLegalActions(self.index):
      nxt = myPos if action == Directions.STOP else self.nextPosition(myPos, action)
      if not self.isLegalPosition(nxt):
        continue
      score = 0
      if action == Directions.STOP:
        score -= 8
      if action == reverse:
        score -= 1
      if nxt in avoid:
        score -= 10000
      if targets:
        score -= self.minDistance(nxt, targets)
      if ghosts:
        score += 8 * self.minDistance(nxt, ghosts)

      if bestScore is None or score > bestScore:
        bestAction = action
        bestScore = score

    return bestAction

  def nearestTargets(self, pos, targets, limit=None):
    ordered = sorted(targets, key=lambda target: self.getMazeDistance(pos, target))
    if limit is None:
      return ordered
    return ordered[:limit]


class PlanningOffensiveAgent(PlanningCaptureAgent):
  def chooseAction(self, gameState):
    myState = gameState.getAgentState(self.index)
    myPos = self.cleanPosition(gameState.getAgentPosition(self.index))
    foodList = self.getFood(gameState).asList()
    capsules = self.getCapsules(gameState)
    ghosts = self.activeEnemyGhostPositions(gameState)
    ghostDistance = self.minDistance(myPos, ghosts) if ghosts else 999
    homeDistance = self.minDistance(myPos, self.portals)
    carrying = myState.numCarrying

    avoidWide = self.dangerZones(gameState, 1)
    avoidTight = set(ghosts)

    if carrying > 0 and gameState.data.timeleft < homeDistance + 25:
      return self.moveToTargets(gameState, self.portals, avoidTight)
    if carrying >= 4 or len(foodList) <= 2:
      return self.moveToTargets(gameState, self.portals, avoidTight)
    if carrying > 0 and ghostDistance <= 5:
      return self.moveToTargets(gameState, self.portals, avoidWide)
    if ghostDistance <= 2:
      if capsules:
        return self.moveToTargets(gameState, capsules, avoidTight)
      return self.moveToTargets(gameState, self.portals, avoidTight)
    if ghostDistance <= 4 and capsules:
      return self.moveToTargets(gameState, capsules, avoidTight)

    if foodList:
      targets = self.chooseFoodTargets(myPos, foodList)
      return self.moveToTargets(gameState, targets, avoidWide)

    return self.moveToTargets(gameState, self.portals, avoidTight)

  def moveToTargets(self, gameState, targets, avoid):
    action = self.choosePathAction(gameState, targets, avoid=avoid, allowStop=False)
    if action == Directions.STOP and avoid:
      action = self.choosePathAction(gameState, targets, avoid=set(), allowStop=False)
    return action

  def chooseFoodTargets(self, myPos, foodList):
    def foodScore(food):
      return self.getMazeDistance(myPos, food) + 0.35 * self.minDistance(food, self.portals)

    return sorted(foodList, key=foodScore)[:8]


class PlanningPortalDefender(PlanningCaptureAgent):
  def registerInitialState(self, gameState):
    PlanningCaptureAgent.registerInitialState(self, gameState)
    self.currentTarget = self.chooseFoodClusterPortal(gameState)
    self.lastMissingFood = None

  def chooseAction(self, gameState):
    self.updateMissingFood(gameState)
    myState = gameState.getAgentState(self.index)
    invaders = self.visibleInvaderPositions(gameState)

    supportTargets = self.chooseSupportOffenseTargets(gameState, invaders)
    if supportTargets:
      avoid = self.dangerZones(gameState, 1)
      if myState.numCarrying > 0:
        avoid = set(self.activeEnemyGhostPositions(gameState))
      action = self.choosePathAction(
        gameState,
        supportTargets,
        avoid=avoid,
        allowStop=False
      )
      if action != Directions.STOP or not avoid:
        return action
      return self.choosePathAction(
        gameState,
        supportTargets,
        avoid=set(),
        allowStop=False
      )

    self.currentTarget = self.chooseDefensiveTarget(gameState, invaders)

    avoid = set()
    allowStop = self.currentTarget in self.portals and not invaders and not myState.isPacman
    if myState.scaredTimer > 0:
      avoid = set(invaders)

    return self.choosePathAction(
      gameState,
      [self.currentTarget],
      avoid=avoid,
      allowStop=allowStop
    )

  def chooseSupportOffenseTargets(self, gameState, invaders):
    myState = gameState.getAgentState(self.index)
    myPos = self.cleanPosition(gameState.getAgentPosition(self.index))

    if (self.width, self.height) != (38, 13):
      return []
    if myPos is None:
      return []
    if invaders or self.lastMissingFood is not None or myState.scaredTimer > 0:
      return []
    if self.getScore(gameState) > 1 or gameState.data.timeleft < 90:
      return []
    if myState.numCarrying > 0:
      return self.portals

    portalDistance = self.minDistance(myPos, self.portals)
    if not myState.isPacman and portalDistance > 3:
      return []

    ghosts = self.activeEnemyGhostPositions(gameState)
    if ghosts and self.minDistance(myPos, ghosts) <= 4:
      return []

    return self.nearBoundaryFoodTargets(gameState, myPos, ghosts)

  def nearBoundaryFoodTargets(self, gameState, myPos, ghosts):
    foodList = self.getFood(gameState).asList()
    if not foodList:
      return []

    candidates = []
    for food in foodList:
      portalDistance = self.minDistance(food, self.portals)
      if portalDistance > 6:
        continue
      if ghosts and self.minDistance(food, ghosts) <= 3:
        continue
      candidates.append((food, portalDistance))

    def foodScore(item):
      food, portalDistance = item
      return self.getMazeDistance(myPos, food) + 0.8 * portalDistance

    return [food for food, portalDistance in sorted(candidates, key=foodScore)[:4]]

  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, invaders):
    myPos = self.cleanPosition(gameState.getAgentPosition(self.index))
    myState = gameState.getAgentState(self.index)

    if myState.isPacman:
      return min(self.portals, key=lambda portal: self.getMazeDistance(myPos, portal))
    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.chooseFoodClusterPortal(gameState)

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

    for invaderPos in invaders:
      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)

      if myState.scaredTimer > 0:
        target = nearestPortal
        score = myToPortal
      elif myToInvader <= 4 or myToInvader <= invaderToPortal + 1:
        target = invaderPos
        score = myToInvader
      else:
        target = nearestPortal
        score = myToPortal + 0.5 * invaderToPortal

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

    return bestTarget

  def chooseFoodClusterPortal(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)