diff --git a/README.md b/README.md index f21c1f5..6493dd7 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# pacman-capture-flag-contest \ No newline at end of file +# Pacman-capture-flag-contest +Attack and Defense agent for the Pacman intrauniversities contest +(Best ranking: 11th place out of 32) + +## Team + +| **Name | **GitHub** | +| :---: | :---: | +| `Roberta` | [![name](https://github.com/b-rbmp/NexxGate/blob/main/docs/logos/github.png)](https://github.com/RobCTs) | +| `Nazanin` | [![name](https://github.com/b-rbmp/NexxGate/blob/main/docs/logos/github.png)](https://github.com/Naominickels) | + +### Introduction + +The Eutopia Pacman contest is an activity consisting of a multiplayer capture-the-flag variant of Pacman, where agents control both Pacman and ghosts in coordinated team-based strategies. Students from different EUTOPIA universities compete with each other through their programmed agents. +The project is based on the material from the CS188 course Introduction to Arti cial Intelligence at Berkeley2, which was extended for the AI course in 2017 by lecturer Prof. Sebastian Sardina at the Royal Melbourne Institute of Technology (RMIT University) and Dr. Nir Lipovetzky at University of Melbourne +(UoM)3. UPF has refactored the RMIT and UoM code. All the source code is written in Python. + +### Rules of Pacman Capture the Flag + +**2.1 Layout**: +The Pacman map is now divided into two halves: blue (right) and red (left). Red agents (which all have even indices) must defend the red food while trying to eat the blue food. When on the red side, a red agent is a ghost. When crossing into enemy territory, the agent becomes a Pacman. + +**2.2 Scoring:** +As a Pacman eats food dots, those food dots are stored up inside of that Pacman and removed from the board. When a Pacman returns to his side of the board, he \deposits" the food dots he is carrying, earning one point per food pellet delivered. Red team scores are positive, while Blue team scores are negative. +If Pacman is eaten by a ghost before reaching his own side of the board, he will explode into a cloud of food dots that will be deposited back onto the board. + +**2.3 Eating Pacman:** +When a Pacman is eaten by an opposing ghost, the Pacman returns to its starting position (as a ghost). No points are awarded for eating an opponent. + +**2.4 Power Capsules:** +If Pacman eats a power capsule, agents on the opposing team become \scared" for the next 40 moves, or until they are eaten and respawn, whichever comes sooner. Agents that are \scared" are susceptible while in the form of ghosts (i.e. while on their own team's side) to being eaten by Pacman. Speci cally, if Pacman collides with a \scared" ghost, Pacman is una ected and the ghost respawns at its starting position (no longer in the \scared" state). + +**2.5 Observations:** +Agents can only observe an opponent's con guration (position and direction) if they or their teammate is within 5 squares (Manhattan distance). In addition, an agent always gets a noisy distance reading for each agent on the board, which can be used to approximately locate unobserved opponents. + +**2.6 Winning:** +A game ends when one team returns all but two of the opponents' dots. Games are also limited to 1200 agent moves (300 moves per each of the four agents). If this move limit is reached, whichever team has returned the most food wins. If the score is zero (i.e., tied) this is recorded as a tie game. + +**2.7 Computation Time:** +We will run your submissions on the UPF cluster, SNOW. Tournaments will generate many processes that have to be executed without overloading the system. Therefore, each agent has 1 second to return each action. Each move which does not return within one second will incur a warning. After three warnings, or any single move taking more than 3 seconds, the game is forfeit. There will be an initial start-up allowance of 15 seconds (use the registerInitialState function). If your agent times out or otherwise throws an exception, an error message will be present in the log les, which you can download from the results page. diff --git a/myTeam2.py b/myTeam2.py index 597f5d5..fcaf3b7 100644 --- a/myTeam2.py +++ b/myTeam2.py @@ -22,6 +22,10 @@ import random import contest.util as util +import os +os.system('pip install scikit-learn') +from sklearn.cluster import DBSCAN +import numpy as np from contest.captureAgents import CaptureAgent from contest.game import Directions @@ -189,7 +193,7 @@ def choose_action(self, game_state): v = self.MCTS(game_state, a, 5, 0, numCarrying) values.append(v) maxValue = max(values) - print(maxValue) + #print(maxValue) bestActions = [a for a, v in zip(actions, values) if v == maxValue] return bestActions[0] @@ -319,18 +323,23 @@ def get_weights(self, game_state, action, numCarrying): class DefensiveGoodAgent(GoodCaptureAgent): """ - An agent that keeps its side Pacman-free. Again, + A reflex agent that keeps its side Pacman-free. Again, this is to give you an idea of what a defensive agent could be like. It is not the best or only way to make such an agent. """ + # inharit features from parents + def __init__(self, *args, **kwargs): + super(GoodCaptureAgent, self).__init__(*args, **kwargs) + def get_features(self, game_state, action): features = util.Counter() successor = self.get_successor(game_state, action) my_state = successor.get_agent_state(self.index) my_pos = my_state.get_position() + #print("Current position:", my_pos) # Computes whether we're on defense (1) or offense (0) features['on_defense'] = 1 @@ -343,12 +352,146 @@ def get_features(self, game_state, action): if len(invaders) > 0: dists = [self.get_maze_distance(my_pos, a.get_position()) for a in invaders] features['invader_distance'] = min(dists) + # Implementation + else: + # Patrolling strategy when no invaders are visible + features['patrol_distance'] = self.get_patrol_distance(successor) #changed it from game_state + # Encoding the actions if we need to use it for rewards if action == Directions.STOP: features['stop'] = 1 rev = Directions.REVERSE[game_state.get_agent_state(self.index).configuration.direction] if action == rev: features['reverse'] = 1 return features + + + # Defining patrol points + def get_patrol_points(self, game_state): + """ + Identify dynamic patrol points focusing on areas near remaining food + and the nearest power capsule position. + """ + patrol_points = [] + + food_list = self.get_food(game_state).as_list() + nearest_food_in_cluster = self.cluster_food(game_state, food_list) + patrol_points.append(nearest_food_in_cluster) + + + # Include additional strategic points like the nearest power capsule position + power_capsule_position = self.get_power_capsule_position(game_state) + if power_capsule_position: + patrol_points.append(power_capsule_position) + + return patrol_points + + #patrolling strategies + def get_patrol_distance(self, game_state): + """ + Calculate the average distance to key patrol points. + """ + my_state = game_state.get_agent_state(self.index) + my_pos = my_state.get_position() + #print("Current positionPatrol:", my_pos) + + # Define key patrol points (static or dynamically determined) + patrol_points = self.get_patrol_points(game_state) + + # Calculate distances to each patrol point + #distances = [self.get_maze_distance(tuple(my_pos), tuple(point)) for point in patrol_points] # point is a np.array, but it needs to be a tuple + distances = [self.get_maze_distance(tuple(map(int, my_pos)), tuple(map(int, point))) for point in patrol_points] + + # Return the average distance + if distances: + return sum(distances) / len(distances) + else: + return 0 + + + def cluster_food(self, game_state, food_list, eps=3, min_samples=2): + """ + Cluster food pellets using DBSCAN. + + :param food_list: List of food pellet coordinates. + :param eps: The maximum distance between two samples for one to be considered as in the neighborhood of the other. + :param min_samples: The number of samples in a neighborhood for a point to be considered as a core point. + :return: List of clusters with their food pellet coordinates. + """ + # Convert food_list to a numpy array for DBSCAN + food_array = np.array(food_list) + + # Apply DBSCAN clustering + dbscan = DBSCAN(eps=eps, min_samples=min_samples) + dbscan.fit(food_array) + + # Extract clustered food pellets + clusters = [food_array[dbscan.labels_ == label] for label in set(dbscan.labels_) if label != -1] + + if not clusters: + return None + + # Find the largest cluster + largest_cluster = max(clusters, key=len) + + # Get current position of the agent + my_pos = game_state.get_agent_state(self.index).get_position() + + # Find the nearest food in the largest cluster + nearest_food = min(largest_cluster, key=lambda food: self.get_maze_distance(my_pos, tuple(food))) + + return tuple(nearest_food) + + + def get_power_capsule_position(self, game_state): + """ + Find and return the position of the nearest power capsule. + """ + my_state = game_state.get_agent_state(self.index) + my_pos = my_state.get_position() + capsules = game_state.get_capsules() + + if capsules: + return min(capsules, key=lambda pos: self.get_maze_distance(my_pos, pos)) + else: + return None + def get_weights(self, game_state, action): - return {'num_invaders': -1000, 'on_defense': 100, 'invader_distance': -10, 'stop': -100, 'reverse': -2} + """ + Dynamically adjust weights based on the current game state. + """ + + # Default weights + weights = { + 'num_invaders': -1000, + 'on_defense': 100, + 'invader_distance': -10, + 'stop': -100, + 'reverse': -2, + 'patrol_distance': -5 # Weight for patrol distance + } + + # Adjust weights based on specific game state conditions + my_state = game_state.get_agent_state(self.index) + my_pos = my_state.get_position() + enemies = [game_state.get_agent_state(i) for i in self.get_opponents(game_state)] + invaders = [a for a in enemies if a.is_pacman and a.get_position() is not None] + + # Example: Increase the penalty for stopping if there are invaders close by + if invaders: + closest_invader_distance = min([self.get_maze_distance(my_pos, a.get_position()) for a in invaders]) + if closest_invader_distance < 5: # If an invader is very close + weights['stop'] -= 50 # Increase the penalty for stopping + + # Example: Adjust weights based on the remaining food and moves + remaining_food = len(self.get_food_you_are_defending(game_state).as_list()) + remaining_moves = game_state.data.timeleft + total_moves = 1200 # Total moves before the game ends + + if remaining_food <= 4 or remaining_moves < total_moves / 4: + weights['num_invaders'] *=3 + weights['on_defense'] *= 2 + weights['patrol_distance'] *= 1 + + return weights +