Code for How to Make a Pacman Game with Python Tutorial


View on Github

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))