├── Dockerfile ├── .gitignore ├── .github └── workflows │ └── github.yml ├── ghosts.py ├── utility.py ├── play.py ├── README.md ├── game.py └── pacman.py /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image 2 | FROM python:3.11-slim 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the Python file into the container 8 | COPY . . 9 | 10 | # Run the Python file 11 | CMD ["python", "play.py"] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Virtual environment 7 | .venv/ 8 | venv/ 9 | ENV/ 10 | env/ 11 | 12 | # Distribution / packaging 13 | build/ 14 | dist/ 15 | *.egg-info/ 16 | .eggs/ 17 | 18 | # Installer logs 19 | pip-log.txt 20 | pip-delete-this-directory.txt 21 | 22 | # Unit test / coverage reports 23 | .coverage 24 | .tox/ 25 | .nox/ 26 | .pytest_cache/ 27 | htmlcov/ 28 | 29 | # Jupyter Notebook 30 | .ipynb_checkpoints 31 | 32 | # Django / Flask stuff 33 | *.log 34 | local_settings.py 35 | db.sqlite3 36 | instance/ 37 | .webassets-cache 38 | 39 | # VSCode / PyCharm / IDE settings 40 | .vscode/ 41 | .idea/ 42 | 43 | # System files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /.github/workflows/github.yml: -------------------------------------------------------------------------------- 1 | name: Python CI with Docker 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | lint-and-docker: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Checkout repo 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | 18 | # Set up Python 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.11" 23 | 24 | # Install linting tool 25 | - name: Install flake8 26 | run: pip install flake8 27 | 28 | # Run lint check 29 | - name: Run flake8 30 | run: flake8 play.py 31 | 32 | # Build Docker image 33 | - name: Build Docker image 34 | run: docker build -t pacman . 35 | 36 | # Run Docker container 37 | - name: Run Docker container 38 | run: docker run --rm pacman 39 | -------------------------------------------------------------------------------- /ghosts.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | import random as r 3 | import time as t 4 | 5 | 6 | class ghosts: 7 | def __init__(self): 8 | pass 9 | 10 | def move_ghosts( 11 | self, pos_ghosts1, pos_ghosts2, board, board_size, random=True, action=None 12 | ) -> Tuple: 13 | new_pos_ghosts1 = self.randomally_moves(pos_ghosts1, board_size, random, action) 14 | new_pos_ghosts2 = self.randomally_moves(pos_ghosts2, board_size, random, action) 15 | 16 | if self._is_not_possible(new_pos_ghosts1, board, board_size): 17 | new_pos_ghosts1 = pos_ghosts1 18 | 19 | if self._is_not_possible(new_pos_ghosts2, board, board_size): 20 | new_pos_ghosts2 = pos_ghosts2 21 | 22 | return {"ghosts1": new_pos_ghosts1, "ghosts2": new_pos_ghosts2} 23 | 24 | def randomally_moves(self, pos, board_size, random=True, action=None) -> Tuple: 25 | ways = {"up": (-1, 0), "down": (1, 0), "left": (0, -1), "right": (0, 1)} 26 | 27 | if random: 28 | way_key = r.choice(list(ways.keys())) 29 | way = ways[way_key] 30 | 31 | x, y = pos 32 | new_x = x + way[0] 33 | new_y = y + way[1] 34 | 35 | else: 36 | x, y = pos 37 | way = ways[action] 38 | new_x = x + way[0] 39 | new_y = y + way[1] 40 | 41 | if 0 <= new_x < board_size[0] and 0 <= new_y < board_size[1]: 42 | """ 43 | Here it is removed from the game screen, 44 | so it is better to stay in place 45 | """ 46 | 47 | return (new_x, new_y) 48 | else: 49 | return pos 50 | 51 | def _is_not_possible(self, pos, board, board_size) -> True: 52 | x, y = pos 53 | if 0 <= x < board_size[0] and 0 <= y < board_size[1] and board[x][y] != "-": 54 | return False 55 | else: 56 | return True 57 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | class utility: 2 | def __init__(self) -> None: 3 | pass 4 | 5 | def get_utility(self, state): 6 | if self.is_pacman_win(state): 7 | return 1000 + state.score 8 | 9 | if state.get_pos_pacman() == state.get_pos_ghost( 10 | 1 11 | ) or state.get_pos_pacman() == state.get_pos_ghost(2): 12 | return state.score - 1000 13 | 14 | return ( 15 | state.score 16 | + self.distance_from_near_food(state) * 100 17 | + self.distance_from_near_food(state) * 1000 18 | ) 19 | 20 | def is_game_finished(self, state): 21 | return ( 22 | self.is_pacman_win(state) 23 | or state.get_pos_pacman() == state.get_pos_ghost(1) 24 | or state.get_pos_pacman() == state.get_pos_ghost(2) 25 | ) 26 | 27 | def is_pacman_win(self, state): 28 | for row in state.get_board(): 29 | if "*" in row: 30 | return False 31 | return True 32 | 33 | def distance_from_near_food(self, state): 34 | board = state.get_board() 35 | size = state.get_size() 36 | pos_pacman = state.get_pos_pacman() 37 | 38 | sizes = list() 39 | for i in range(size[0]): 40 | for j in range(size[1]): 41 | if board[i][j] == "*": 42 | sizes.append(self._euclidean_distance((i, j), pos_pacman)) 43 | return min(sizes) 44 | 45 | def distance_from_near_food(self, state): 46 | pos_pacman = state.get_pos_pacman() 47 | ghosts1 = state.get_pos_ghost(1) 48 | ghosts2 = state.get_pos_ghost(2) 49 | 50 | ed1, ed2 = self._euclidean_distance( 51 | pos_pacman, ghosts1 52 | ), self._euclidean_distance(pos_pacman, ghosts2) 53 | return min(ed1, ed2) 54 | 55 | def _euclidean_distance(self, point1, point2): 56 | import math as m 57 | 58 | return m.sqrt(pow(point1[0] - point1[0], 2) + pow(point2[0] - point2[0], 2)) 59 | -------------------------------------------------------------------------------- /play.py: -------------------------------------------------------------------------------- 1 | from pacman import pacman 2 | from game import game 3 | from ghosts import ghosts 4 | from utility import utility 5 | 6 | 7 | class play: 8 | def __init__(self) -> None: 9 | self.game = game() 10 | self.score = self.game.score 11 | self.ghosts = ghosts() 12 | self.state = self.game.get_board() 13 | self.utility = utility() 14 | self.pacman = pacman() 15 | 16 | def start(self): 17 | while True: 18 | self.game.display(self.game.get_score()) 19 | if self.utility.is_game_finished(self.game): 20 | if self.utility.is_pacman_win(self.game): 21 | print("** PACMAN IS WIN !!!") 22 | else: 23 | print("PACMAN LOST !") 24 | break 25 | best_action = self.pacman.best_action(self.game) 26 | new_pos_pacmans = self.pacman.moves_pacman( 27 | self.game.get_pos_pacman(), 28 | best_action, 29 | self.game.get_size(), 30 | self.game.get_board(), 31 | ) 32 | 33 | self.game.set_pos_pacman(new_pos_pacmans) 34 | 35 | pos_g1 = self.game.get_pos_ghost(1) 36 | pos_g2 = self.game.get_pos_ghost(2) 37 | 38 | self.game.score -= 1 39 | 40 | poses = self.ghosts.move_ghosts( 41 | pos_g1, pos_g2, self.game.get_board(), self.game.get_size() 42 | ) 43 | 44 | self.game.set_pos_ghost(1, poses["ghosts1"]) 45 | self.game.set_pos_ghost(2, poses["ghosts2"]) 46 | 47 | if self.game.get_pos_pacman() == self.game.get_pos_ghost( 48 | 1 49 | ) or self.game.get_pos_pacman() == self.game.get_pos_ghost(2): 50 | print("Pacman is caught by a ghost!") 51 | break 52 | 53 | if ( 54 | self.game.get_board()[self.game.get_pos_pacman()[0]][ 55 | self.game.get_pos_pacman()[1] 56 | ] 57 | == "*" 58 | ): 59 | self.game.score += 10 60 | board = self.game.get_board() 61 | board[self.game.get_pos_pacman()[0]][ 62 | self.game.get_pos_pacman()[1] 63 | ] = " " 64 | self.game.set_board(board) 65 | 66 | 67 | run = play() 68 | run.start() 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pacman Game with AI 2 | 3 | A Pacman game implementation with an AI player using the Minimax algorithm. This project showcases the classic Pacman game environment, where the player (Pacman) navigates a maze to collect points while avoiding ghosts. The AI-controlled ghosts aim to catch Pacman. 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Game Features](#game-features) 9 | - [Implemented Classes](#implemented-classes) 10 | - [How to Play](#how-to-play) 11 | - [AI Algorithm - Minimax](#ai-algorithm---minimax) 12 | - [Project Structure](#project-structure) 13 | - [Running the Game](#running-the-game) 14 | - [Contributing](#contributing) 15 | - [License](#license) 16 | 17 | ## Introduction 18 | 19 | This project provides a Python implementation of a Pacman game with an AI-controlled Pacman. The game board is randomly generated, and the player's goal is to collect points while avoiding ghosts. The AI-controlled ghosts move randomly, and the game ends if Pacman is caught. 20 | 21 | ## Game Features 22 | 23 | - **Randomly Generated Board**: The game board is randomly generated with walls and points for Pacman to collect. 24 | - **AI-Controlled Ghosts**: The ghosts move randomly on the board, creating a challenge for Pacman. 25 | - **Score Tracking**: The game tracks the player's score, increasing for each point collected and decreasing if caught by a ghost. 26 | - **Game Over**: The game ends if Pacman is caught by a ghost or if all points are collected. 27 | 28 | ## Implemented Classes 29 | 30 | - **Game Class**: Manages the game state, including the board, Pacman and ghost positions, and the player's score. 31 | - **Ghosts Class**: Controls the movement of the ghosts, either randomly or based on specified actions. 32 | - **Pacman Class**: Implements the Minimax algorithm for Pacman's moves and evaluates the utility of different game states. 33 | - **Play Class**: Handles the game loop, player input, and updates to the game state. 34 | - **Utility Class**: Provides utility functions for calculating game state utilities, checking game completion, and evaluating distances. 35 | 36 | ## How to Play 37 | 38 | The game is played in the console. Pacman's movements are controlled by the AI, and the player can observe the game state and score. 39 | 40 | ## AI Algorithm - Minimax 41 | 42 | Pacman's movements are determined using the Minimax algorithm. The algorithm evaluates possible future game states, considering both Pacman and the ghosts, to make the optimal move that maximizes the utility of the game state. 43 | 44 | ## Project Structure 45 | 46 | - `game.py`: Contains the main Game class for managing the game state. 47 | - `ghosts.py`: Implements the Ghosts class for controlling ghost movements. 48 | - `pacman.py`: Defines the Pacman class, which uses the Minimax algorithm for decision-making. 49 | - `play.py`: Handles the game loop and user interface. 50 | - `utility.py`: Provides utility functions for evaluating game states. 51 | 52 | ## Running the Game 53 | 54 | To run the game, execute the `play.py` script. Ensure that Python and the required dependencies are installed. 55 | 56 | ```bash 57 | python play.py 58 | ``` 59 | 60 | ## Contributing 61 | 62 | Contributions are welcome! If you would like to contribute to this project, please follow our [contribution guidelines](CONTRIBUTING.md). 63 | 64 | ## License 65 | 66 | This project is licensed under the [MIT License](LICENSE). 67 | 68 | -------------------------------------------------------------------------------- /game.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List 2 | import random as r 3 | 4 | 5 | class game: 6 | """ 7 | for frist arg ----> refs to Y 8 | for secend arg ----> refs to X 9 | """ 10 | 11 | def __init__( 12 | self, 13 | size_board=(9, 18), 14 | number_of_wall=50, 15 | pacman_position=(6, 16), 16 | ghost1_position=None, 17 | ghost2_position=None, 18 | score=0, 19 | ) -> None: 20 | self.size_board = size_board 21 | self.number_of_wall = number_of_wall 22 | self.pacman_position = pacman_position 23 | self.ghost1_position = (9, 15) 24 | self.ghost2_position = (5, 13) 25 | self.score = score 26 | 27 | self.board = None 28 | self.create_board_game() 29 | 30 | def get_score(self) -> int: 31 | return self.score 32 | 33 | def get_pos_pacman(self) -> Tuple: 34 | return self.pacman_position 35 | 36 | def get_pos_ghost(self, choice: int) -> Tuple: 37 | if choice == 1: 38 | return self.ghost1_position 39 | elif choice == 2: 40 | return self.ghost2_position 41 | 42 | def get_board(self) -> List: 43 | return self.board 44 | 45 | def set_board(self, board) -> None: 46 | self.board = board 47 | 48 | def set_pos_pacman(self, new_pos) -> None: 49 | self.pacman_position = new_pos 50 | 51 | def set_pos_ghost(self, choice: int, new_pos) -> Tuple: 52 | if choice == 1: 53 | self.ghost1_position = new_pos 54 | elif choice == 2: 55 | self.ghost2_position = new_pos 56 | 57 | def set_score(self, score): 58 | self.score = score 59 | 60 | def get_size(self): 61 | return self.size_board 62 | 63 | def create_board_game(self) -> None: 64 | self.board = [ 65 | ["*" for _ in range(self.size_board[1])] for _ in range(self.size_board[0]) 66 | ] 67 | 68 | counter = 1 69 | while counter <= self.number_of_wall: 70 | i, j = r.randint(0, self.size_board[0] - 1), r.randint( 71 | 0, self.size_board[1] - 1 72 | ) 73 | if ( 74 | (i, j) == self.pacman_position 75 | or (i, j) == self.ghost1_position 76 | or (i, j) == self.ghost2_position 77 | or self.board[i][j] == "-" 78 | ): 79 | continue 80 | 81 | self.board[i][j] = "-" 82 | counter += 1 83 | 84 | del counter 85 | 86 | def display(self, score: int) -> None: 87 | for x in range(self.size_board[0]): 88 | for y in range(self.size_board[1]): 89 | if (x, y) == self.pacman_position: 90 | print("P", end=" ") 91 | elif (x, y) == self.ghost1_position: 92 | print("G1", end=" ") 93 | elif (x, y) == self.ghost2_position: 94 | print("G2", end=" ") 95 | else: 96 | print(self.board[x][y], end=" ") 97 | print() 98 | print(f" Score: {self.score}") 99 | 100 | def get_pos_pacman(self) -> Tuple: 101 | return self.pacman_position 102 | 103 | def get_pos_ghost(self, choice: int) -> Tuple: 104 | if choice == 1: 105 | return self.ghost1_position 106 | elif choice == 2: 107 | return self.ghost2_position 108 | -------------------------------------------------------------------------------- /pacman.py: -------------------------------------------------------------------------------- 1 | from utility import utility 2 | from game import game 3 | from ghosts import ghosts 4 | 5 | from typing import List 6 | import random as r 7 | 8 | 9 | class pacman: 10 | def __init__(self): 11 | self._ways_possible_for_ghosts = [ 12 | "up", 13 | "down", 14 | "left", 15 | "right", 16 | ] 17 | self.utility = utility() 18 | 19 | def _min_max( 20 | self, state, is_pacman, depth, alpha=float("-inf"), beta=float("-inf") 21 | ) -> set: 22 | if self.utility.is_game_finished(state) or depth == 0: 23 | return self.utility.get_utility(state) 24 | 25 | if is_pacman: 26 | v = float("-inf") 27 | actions_values = [ 28 | (action, self._min_max(self.transfer(state, action, 1), 0, depth - 1)) 29 | for action in self._ways_possible_for_pacman(state) 30 | ] 31 | v = max(actions_values, key=lambda x: x[1])[1] 32 | if v >= beta: 33 | return v 34 | alpha = max(alpha, v) 35 | return v 36 | else: 37 | actions_values = [ 38 | (action, self._min_max(self.transfer(state, action, 0), 1, depth - 1)) 39 | for action in self._ways_possible_for_ghosts 40 | ] 41 | v = min(actions_values, key=lambda x: x[1])[1] 42 | if v <= alpha: 43 | return v 44 | beta = min(beta, v) 45 | return v 46 | 47 | def best_action(self, state): 48 | actions_values = [ 49 | ( 50 | action, 51 | self._min_max( 52 | self.transfer(create_copy_state(state), action, 1), 53 | 0, 54 | depth=5, 55 | ), 56 | ) 57 | for action in self._ways_possible_for_pacman(state) 58 | ] 59 | best_word = max(actions_values, key=lambda temp: temp[1]) 60 | best_action, best_score = best_word 61 | best_action = [ 62 | elements[0] for elements in actions_values if elements[1] == best_score 63 | ] 64 | return r.choice(best_action) 65 | 66 | def transfer(self, state, action, is_pacman): 67 | if is_pacman: 68 | new_state = create_copy_state(state) 69 | xp, yp = state.get_pos_pacman() 70 | 71 | board = state.get_board() 72 | board_size = state.get_size() 73 | new_pos_pacman = self.moves_pacman((xp, yp), action, board_size, board) 74 | new_state.set_pos_pacman(new_pos_pacman) 75 | return new_state 76 | 77 | else: 78 | G = ghosts() 79 | new_state = create_copy_state(state) 80 | pos_g1 = state.get_pos_ghost(1) 81 | pos_g2 = state.get_pos_ghost(2) 82 | 83 | board = state.get_board() 84 | board_size = state.get_size() 85 | 86 | new_pos_ghosts = G.move_ghosts( 87 | pos_g1, pos_g2, board, board_size, False, action 88 | ) 89 | 90 | new_pos_g1 = new_pos_ghosts["ghosts1"] 91 | new_pos_g2 = new_pos_ghosts["ghosts2"] 92 | 93 | new_state.set_pos_ghost(1, new_pos_g1) 94 | new_state.set_pos_ghost(2, new_pos_g2) 95 | 96 | return new_state 97 | 98 | def _ways_possible_for_pacman( 99 | self, 100 | state, 101 | ) -> List: 102 | x, y = state.get_pos_pacman() 103 | board = state.get_board() 104 | board_size = state.get_size() 105 | 106 | actions = [] 107 | 108 | if x > 0 and board[x - 1][y] != "-": 109 | actions.append("up") 110 | 111 | if x < board_size[0] - 1 and board[x + 1][y] != "-": 112 | actions.append("down") 113 | 114 | if y > 0 and board[x][y - 1] != "-": 115 | actions.append("left") 116 | 117 | if y < board_size[1] - 1 and board[x][y + 1] != "-": 118 | actions.append("right") 119 | return actions 120 | 121 | def moves_pacman(self, pacman_pos, way_posibale, board_size, board) -> set: 122 | x, y = pacman_pos 123 | ways = {"up": (-1, 0), "down": (1, 0), "left": (0, -1), "right": (0, 1)} 124 | dx, dy = ways[way_posibale] 125 | new_x = x + dx 126 | new_y = y + dy 127 | 128 | if ( 129 | 0 <= new_x < board_size[0] 130 | and 0 <= new_y < board_size[1] 131 | and board[x][y] != "-" 132 | ): 133 | return (new_x, new_y) 134 | 135 | else: 136 | pacman_pos 137 | 138 | 139 | def create_copy_state(state) -> game: 140 | new_board = game() 141 | new_board.set_pos_pacman(state.get_pos_pacman()) 142 | new_board.set_pos_ghost(1, state.get_pos_ghost(1)) 143 | new_board.set_pos_ghost(2, state.get_pos_ghost(2)) 144 | new_board.set_score(state.get_score()) 145 | new_board.set_board(state.get_board()) 146 | return new_board 147 | --------------------------------------------------------------------------------