cell.py
import pygame
class Cell(pygame.sprite.Sprite):
def __init__(self, row, col, length, width):
super().__init__()
self.width = length
self.height = width
self.id = (row, col)
self.abs_x = row * self.width
self.abs_y = col * self.height
self.rect = pygame.Rect(self.abs_x,self.abs_y,self.width,self.height)
self.occupying_piece = None
def update(self, screen):
pygame.draw.rect(screen, pygame.Color("blue2"), self.rect)
berry.py
import pygame
from settings import CHAR_SIZE, PLAYER_SPEED
class Berry(pygame.sprite.Sprite):
def __init__(self, row, col, size, is_power_up = False):
super().__init__()
self.power_up = is_power_up
self.size = size
self.color = pygame.Color("violetred")
self.thickness = size
self.abs_x = (row * CHAR_SIZE) + (CHAR_SIZE // 2)
self.abs_y = (col * CHAR_SIZE) + (CHAR_SIZE // 2)
# temporary rect for colliderect-checking
self.rect = pygame.Rect(self.abs_x,self.abs_y, self.size * 2, self.size * 2)
def update(self, screen):
self.rect = pygame.draw.circle(screen, self.color, (self.abs_x, self.abs_y), self.size, self.thickness)
main.py
import pygame, sys
from settings import WIDTH, HEIGHT, NAV_HEIGHT
from world import World
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT + NAV_HEIGHT))
pygame.display.set_caption("PacMan")
class Main:
def __init__(self, screen):
self.screen = screen
self.FPS = pygame.time.Clock()
def main(self):
world = World(self.screen)
while True:
self.screen.fill("black")
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
world.update()
pygame.display.update()
self.FPS.tick(30)
if __name__ == "__main__":
play = Main(screen)
play.main()
settings.py
MAP = [
['1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1'],
['1',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','1'],
['1','B','1','1',' ','1','1','1',' ','1',' ','1','1','1',' ','1','1','B','1'],
['1',' ',' ',' ',' ','1',' ',' ',' ','1',' ',' ',' ','1',' ',' ',' ',' ','1'],
['1','1',' ','1',' ','1',' ','1',' ','1',' ','1',' ','1',' ','1',' ','1','1'],
['1',' ',' ','1',' ',' ',' ','1',' ',' ',' ','1',' ',' ',' ','1',' ',' ','1'],
['1',' ','1','1','1','1',' ','1','1','1','1','1',' ','1','1','1','1',' ','1'],
['1',' ',' ',' ',' ',' ',' ',' ',' ','r',' ',' ',' ',' ',' ',' ',' ',' ','1'],
['1','1',' ','1','1','1',' ','1','1','-','1','1',' ','1','1','1',' ','1','1'],
[' ',' ',' ',' ',' ','1',' ','1','s','p','o','1',' ','1',' ',' ',' ',' ',' '],
['1','1',' ','1',' ','1',' ','1','1','1','1','1',' ','1',' ','1',' ','1','1'],
['1',' ',' ','1',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','1',' ',' ','1'],
['1',' ','1','1','1','1',' ','1','1','1','1','1',' ','1','1','1','1',' ','1'],
['1',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','1'],
['1','1','1',' ','1','1','1',' ','1','1','1',' ','1','1','1',' ','1','1','1'],
['1',' ',' ',' ','1',' ',' ',' ',' ','P',' ',' ',' ',' ','1',' ',' ',' ','1'],
['1','B','1',' ','1',' ','1',' ','1','1','1',' ','1',' ','1',' ','1','B','1'],
['1',' ','1',' ',' ',' ','1',' ',' ',' ',' ',' ','1',' ',' ',' ','1',' ','1'],
['1',' ','1','1','1',' ','1','1','1',' ','1','1','1',' ','1','1','1',' ','1'],
['1',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','1'],
['1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1']
]
BOARD_RATIO = (len(MAP[0]), len(MAP))
CHAR_SIZE = 32
WIDTH, HEIGHT = (BOARD_RATIO[0] * CHAR_SIZE, BOARD_RATIO[1] * CHAR_SIZE)
NAV_HEIGHT = 64
PLAYER_SPEED = CHAR_SIZE // 4
GHOST_SPEED = 4
animation.py
from os import walk
import pygame
def import_sprite(path):
surface_list = []
for _, __, img_file in walk(path):
for image in img_file:
full_path = f"{path}/{image}"
img_surface = pygame.image.load(full_path).convert_alpha()
surface_list.append(img_surface)
return surface_list
ghost.py
import pygame
import random
import time
from settings import WIDTH, CHAR_SIZE, GHOST_SPEED
class Ghost(pygame.sprite.Sprite):
def __init__(self, row, col, color):
super().__init__()
self.abs_x = (row * CHAR_SIZE)
self.abs_y = (col * CHAR_SIZE)
self.rect = pygame.Rect(self.abs_x, self.abs_y, CHAR_SIZE, CHAR_SIZE)
self.move_speed = GHOST_SPEED
self.color = pygame.Color(color)
self.move_directions = [(-1,0), (0,-1), (1,0), (0,1)]
self.moving_dir = "up"
self.img_path = f'assets/ghosts/{color}/'
self.img_name = f'{self.moving_dir}.png'
self.image = pygame.image.load(self.img_path + self.img_name)
self.image = pygame.transform.scale(self.image, (CHAR_SIZE, CHAR_SIZE))
self.rect = self.image.get_rect(topleft = (self.abs_x, self.abs_y))
self.mask = pygame.mask.from_surface(self.image)
self.directions = {'left': (-self.move_speed, 0), 'right': (self.move_speed, 0), 'up': (0, -self.move_speed), 'down': (0, self.move_speed)}
self.keys = ['left', 'right', 'up', 'down']
self.direction = (0, 0)
def move_to_start_pos(self):
self.rect.x = self.abs_x
self.rect.y = self.abs_y
def is_collide(self, x, y, walls_collide_list):
tmp_rect = self.rect.move(x, y)
if tmp_rect.collidelist(walls_collide_list) == -1:
return False
return True
def _animate(self):
self.img_name = f'{self.moving_dir}.png'
self.image = pygame.image.load(self.img_path + self.img_name)
self.image = pygame.transform.scale(self.image, (CHAR_SIZE, CHAR_SIZE))
self.rect = self.image.get_rect(topleft=(self.rect.x, self.rect.y))
def update(self, walls_collide_list):
# ghost movement
available_moves = []
for key in self.keys:
if not self.is_collide(*self.directions[key], walls_collide_list):
available_moves.append(key)
randomizing = False if len(available_moves) <= 2 and self.direction != (0,0) else True
# 60% chance of randomizing ghost move
if randomizing and random.randrange( 0,100 ) <= 60:
self.moving_dir = random.choice(available_moves)
self.direction = self.directions[self.moving_dir]
if not self.is_collide(*self.direction, walls_collide_list):
self.rect.move_ip(self.direction)
else:
self.direction = (0,0)
# teleporting to the other side of the map
if self.rect.right <= 0:
self.rect.x = WIDTH
elif self.rect.left >= WIDTH:
self.rect.x = 0
self._animate()
display.py
import pygame
from settings import WIDTH, HEIGHT, CHAR_SIZE
pygame.font.init()
class Display:
def __init__(self, screen):
self.screen = screen
self.font = pygame.font.SysFont("ubuntumono", CHAR_SIZE)
self.game_over_font = pygame.font.SysFont("dejavusansmono", 48)
self.text_color = pygame.Color("crimson")
def show_life(self, life):
img_path = "assets/life/life.png"
life_image = pygame.image.load(img_path)
life_image = pygame.transform.scale(life_image, (CHAR_SIZE, CHAR_SIZE))
life_x = CHAR_SIZE // 2
if life != 0:
for life in range(life):
self.screen.blit(life_image, (life_x, HEIGHT + (CHAR_SIZE // 2)))
life_x += CHAR_SIZE
def show_level(self, level):
level_x = WIDTH // 3
level = self.font.render(f'Level {level}', True, self.text_color)
self.screen.blit(level, (level_x, (HEIGHT + (CHAR_SIZE // 2))))
def show_score(self, score):
score_x = WIDTH // 3
score = self.font.render(f'{score}', True, self.text_color)
self.screen.blit(score, (score_x * 2, (HEIGHT + (CHAR_SIZE // 2))))
# add game over message
def game_over(self):
message = self.game_over_font.render(f'GAME OVER!!', True, pygame.Color("chartreuse"))
instruction = self.font.render(f'Press "R" to Restart', True, pygame.Color("aqua"))
self.screen.blit(message, ((WIDTH // 4), (HEIGHT // 3)))
self.screen.blit(instruction, ((WIDTH // 4), (HEIGHT // 2)))
world.py
import pygame
import time
from settings import HEIGHT, WIDTH, NAV_HEIGHT, CHAR_SIZE, MAP, PLAYER_SPEED
from pac import Pac
from cell import Cell
from berry import Berry
from ghost import Ghost
from display import Display
class World:
def __init__(self, screen):
self.screen = screen
self.player = pygame.sprite.GroupSingle()
self.ghosts = pygame.sprite.Group()
self.walls = pygame.sprite.Group()
self.berries = pygame.sprite.Group()
self.display = Display(self.screen)
self.game_over = False
self.reset_pos = False
self.player_score = 0
self.game_level = 1
self._generate_world()
# create and add player to the screen
def _generate_world(self):
# renders obstacle from the MAP table
for y_index, col in enumerate(MAP):
for x_index, char in enumerate(col):
if char == "1": # for walls
self.walls.add(Cell(x_index, y_index, CHAR_SIZE, CHAR_SIZE))
elif char == " ": # for paths to be filled with berries
self.berries.add(Berry(x_index, y_index, CHAR_SIZE // 4))
elif char == "B": # for big berries
self.berries.add(Berry(x_index, y_index, CHAR_SIZE // 2, is_power_up=True))
# for Ghosts's starting position
elif char == "s":
self.ghosts.add(Ghost(x_index, y_index, "skyblue"))
elif char == "p":
self.ghosts.add(Ghost(x_index, y_index, "pink"))
elif char == "o":
self.ghosts.add(Ghost(x_index, y_index, "orange"))
elif char == "r":
self.ghosts.add(Ghost(x_index, y_index, "red"))
elif char == "P": # for PacMan's starting position
self.player.add(Pac(x_index, y_index))
self.walls_collide_list = [wall.rect for wall in self.walls.sprites()]
def generate_new_level(self):
for y_index, col in enumerate(MAP):
for x_index, char in enumerate(col):
if char == " ": # for paths to be filled with berries
self.berries.add(Berry(x_index, y_index, CHAR_SIZE // 4))
elif char == "B": # for big berries
self.berries.add(Berry(x_index, y_index, CHAR_SIZE // 2, is_power_up=True))
time.sleep(2)
def restart_level(self):
self.berries.empty()
[ghost.move_to_start_pos() for ghost in self.ghosts.sprites()]
self.game_level = 1
self.player.sprite.pac_score = 0
self.player.sprite.life = 3
self.player.sprite.move_to_start_pos()
self.player.sprite.direction = (0, 0)
self.player.sprite.status = "idle"
self.generate_new_level()
# displays nav
def _dashboard(self):
nav = pygame.Rect(0, HEIGHT, WIDTH, NAV_HEIGHT)
pygame.draw.rect(self.screen, pygame.Color("cornsilk4"), nav)
self.display.show_life(self.player.sprite.life)
self.display.show_level(self.game_level)
self.display.show_score(self.player.sprite.pac_score)
def _check_game_state(self):
# checks if game over
if self.player.sprite.life == 0:
self.game_over = True
# generates new level
if len(self.berries) == 0 and self.player.sprite.life > 0:
self.game_level += 1
for ghost in self.ghosts.sprites():
ghost.move_speed += self.game_level
ghost.move_to_start_pos()
self.player.sprite.move_to_start_pos()
self.player.sprite.direction = (0, 0)
self.player.sprite.status = "idle"
self.generate_new_level()
def update(self):
if not self.game_over:
# player movement
pressed_key = pygame.key.get_pressed()
self.player.sprite.animate(pressed_key, self.walls_collide_list)
# teleporting to the other side of the map
if self.player.sprite.rect.right <= 0:
self.player.sprite.rect.x = WIDTH
elif self.player.sprite.rect.left >= WIDTH:
self.player.sprite.rect.x = 0
# PacMan eating-berry effect
for berry in self.berries.sprites():
if self.player.sprite.rect.colliderect(berry.rect):
if berry.power_up:
self.player.sprite.immune_time = 150 # Timer based from FPS count
self.player.sprite.pac_score += 50
else:
self.player.sprite.pac_score += 10
berry.kill()
# PacMan bumping into ghosts
for ghost in self.ghosts.sprites():
if self.player.sprite.rect.colliderect(ghost.rect):
if not self.player.sprite.immune:
time.sleep(2)
self.player.sprite.life -= 1
self.reset_pos = True
break
else:
ghost.move_to_start_pos()
self.player.sprite.pac_score += 100
self._check_game_state()
# rendering
[wall.update(self.screen) for wall in self.walls.sprites()]
[berry.update(self.screen) for berry in self.berries.sprites()]
[ghost.update(self.walls_collide_list) for ghost in self.ghosts.sprites()]
self.ghosts.draw(self.screen)
self.player.update()
self.player.draw(self.screen)
self.display.game_over() if self.game_over else None
self._dashboard()
# reset Pac and Ghosts position after PacMan get captured
if self.reset_pos and not self.game_over:
[ghost.move_to_start_pos() for ghost in self.ghosts.sprites()]
self.player.sprite.move_to_start_pos()
self.player.sprite.status = "idle"
self.player.sprite.direction = (0,0)
self.reset_pos = False
# for restart button
if self.game_over:
pressed_key = pygame.key.get_pressed()
if pressed_key[pygame.K_r]:
self.game_over = False
self.restart_level()
pac.py
import pygame
from settings import CHAR_SIZE, PLAYER_SPEED
from animation import import_sprite
class Pac(pygame.sprite.Sprite):
def __init__(self, row, col):
super().__init__()
self.abs_x = (row * CHAR_SIZE)
self.abs_y = (col * CHAR_SIZE)
# pac animation
self._import_character_assets()
self.frame_index = 0
self.animation_speed = 0.5
self.image = self.animations["idle"][self.frame_index]
self.rect = self.image.get_rect(topleft = (self.abs_x, self.abs_y))
self.mask = pygame.mask.from_surface(self.image)
self.pac_speed = PLAYER_SPEED
self.immune_time = 0
self.immune = False
self.directions = {'left': (-PLAYER_SPEED, 0), 'right': (PLAYER_SPEED, 0), 'up': (0, -PLAYER_SPEED), 'down': (0, PLAYER_SPEED)}
self.keys = {'left': pygame.K_LEFT, 'right': pygame.K_RIGHT, 'up': pygame.K_UP, 'down': pygame.K_DOWN}
self.direction = (0, 0)
# pac status
self.status = "idle"
self.life = 3
self.pac_score = 0
# gets all the image needed for animating specific player action
def _import_character_assets(self):
character_path = "assets/pac/"
self.animations = {
"up": [],
"down": [],
"left": [],
"right": [],
"idle": [],
"power_up": []
}
for animation in self.animations.keys():
full_path = character_path + animation
self.animations[animation] = import_sprite(full_path)
def _is_collide(self, x, y):
tmp_rect = self.rect.move(x, y)
if tmp_rect.collidelist(self.walls_collide_list) == -1:
return False
return True
def move_to_start_pos(self):
self.rect.x = self.abs_x
self.rect.y = self.abs_y
# update with sprite/sheets
def animate(self, pressed_key, walls_collide_list):
animation = self.animations[self.status]
# loop over frame index
self.frame_index += self.animation_speed
if self.frame_index >= len(animation):
self.frame_index = 0
image = animation[int(self.frame_index)]
self.image = pygame.transform.scale(image, (CHAR_SIZE, CHAR_SIZE))
self.walls_collide_list = walls_collide_list
for key, key_value in self.keys.items():
if pressed_key[key_value] and not self._is_collide(*self.directions[key]):
self.direction = self.directions[key]
self.status = key if not self.immune else "power_up"
break
if not self._is_collide(*self.direction):
self.rect.move_ip(self.direction)
self.status = self.status if not self.immune else "power_up"
if self._is_collide(*self.direction):
self.status = "idle" if not self.immune else "power_up"
def update(self):
# Timer based from FPS count
self.immune = True if self.immune_time > 0 else False
self.immune_time -= 1 if self.immune_time > 0 else 0
self.rect = self.image.get_rect(topleft=(self.rect.x, self.rect.y))