I have been coding ‘PacMan’ with my 16yo, a hopefully not to boring project to help improve his Python coding. We have just moved the ‘Ghosts’ into a Class which was a good first introduction to objects for him.
My coding is far for perfect, and especially not in Python which was only just being invented when I was learning this stuff, so I am looking for feedback on how to improve the professionalism of his code, how to be more ‘pythonic’, how to enable him to add more features to this project. Clearly he has a long way to go, so looking for minor steps forwards that we can understand and work with rather then a whole sale rewrite please 🙂
Ultimately I would like to get him to implement some search algorithms for the ghosts – (BFS, A* etc), so ensuring the current structure is fit to do that within would be good.
Comments, thoughts, suggestions welcomed.
Code is below, or a zip with the textures etc is here: https://filebin.net/nxe82o408swslmyf
It currently runs, but we have not got lives, ghosts killing pacman, levels, etc coded yet.
#imports
import pygame
import os
import time
import math as maths
#Constants
# Text Positioning
CENTRE_MID = 1
LEFT_MID = 2
RIGHT_MID = 3
CENTRE_TOP = 4
LEFT_TOP = 5
RIGHT_TOP = 6
CENTRE_BOT = 7
LEFT_BOT = 8
RIGHT_BOT = 9
#Pacman Orientation
UP = 10
RIGHT = 11
LEFT = 12
DOWN = 13
HYPERJUMPALLOWED = True
HYPERJUMPNOTALLOWED = False
PIXEL = 20
FRAMERATE = 1
# DX and DY for each direction
NORTH = (0, -PIXEL)
SOUTH = (0, PIXEL)
EAST = (PIXEL, 0)
WEST = (-PIXEL, 0)
YELLOW = (255, 255, 102)
PALEYELLOW = (128, 128, 51)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
RED = (255,0,0)
BOARDPIXELWIDTH = 200
BOARDPIXELHEIGHT = 200
#Global Variables
gameOver = False
win = False
score = 0
#Dictionary mapping between board chars and gif's to display.
char_to_image = {'.' : pygame.transform.scale(pygame.image.load('pellet.gif'), (PIXEL, PIXEL)),
'=' : pygame.transform.scale(pygame.image.load('wall-nub.gif'), (PIXEL, PIXEL)),
'=T' : pygame.transform.scale(pygame.image.load('wall-end-b.gif'), (PIXEL, PIXEL)),
'=R' : pygame.transform.scale(pygame.image.load('wall-end-l.gif'), (PIXEL, PIXEL)),
'=L' : pygame.transform.scale(pygame.image.load('wall-end-r.gif'), (PIXEL, PIXEL)),
'=B' : pygame.transform.scale(pygame.image.load('wall-end-t.gif'), (PIXEL, PIXEL)) ,
'=TR' : pygame.transform.scale(pygame.image.load('wall-corner-ll.gif'), (PIXEL, PIXEL)),
'=TL' : pygame.transform.scale(pygame.image.load('wall-corner-lr.gif'), (PIXEL, PIXEL)),
'=BR' : pygame.transform.scale(pygame.image.load('wall-corner-ul.gif'), (PIXEL, PIXEL)),
'=BL' : pygame.transform.scale(pygame.image.load('wall-corner-ur.gif'), (PIXEL, PIXEL)),
'=TB' : pygame.transform.scale(pygame.image.load('wall-straight-vert.gif'), (PIXEL, PIXEL)),
'=RL' : pygame.transform.scale(pygame.image.load('wall-straight-horiz.gif'), (PIXEL, PIXEL)),
'=LTR' : pygame.transform.scale(pygame.image.load('wall-t-bottom.gif'), (PIXEL, PIXEL)),
'=TRB' : pygame.transform.scale(pygame.image.load('wall-t-left.gif'), (PIXEL, PIXEL)),
'=BLT' : pygame.transform.scale(pygame.image.load('wall-t-right.gif'), (PIXEL, PIXEL)),
'=RBL' : pygame.transform.scale(pygame.image.load('wall-t-top.gif'), (PIXEL, PIXEL)),
'=TRLB' : pygame.transform.scale(pygame.image.load('wall-x.gif'), (PIXEL, PIXEL)),
'U' : pygame.transform.scale(pygame.image.load('pacman-u 4.gif'), (PIXEL, PIXEL)),
'R' : pygame.transform.scale(pygame.image.load('pacman-r 4.gif'), (PIXEL, PIXEL)),
'L' : pygame.transform.scale(pygame.image.load('pacman-l 4.gif'), (PIXEL, PIXEL)),
'D' : pygame.transform.scale(pygame.image.load('pacman-d 4.gif'), (PIXEL, PIXEL)),
'!P' : pygame.transform.scale(pygame.image.load('Pinky.gif'), (PIXEL, PIXEL)),
'!P.' : pygame.transform.scale(pygame.image.load('Pinky.gif'), (PIXEL, PIXEL)),
'!B' : pygame.transform.scale(pygame.image.load('Blinky.gif'), (PIXEL, PIXEL)),
'!B.' : pygame.transform.scale(pygame.image.load('Blinky.gif'), (PIXEL, PIXEL)),
'!I' : pygame.transform.scale(pygame.image.load('Inky.gif'), (PIXEL, PIXEL)),
'!I.' : pygame.transform.scale(pygame.image.load('Inky.gif'), (PIXEL, PIXEL)),
'!C' : pygame.transform.scale(pygame.image.load('Clyde.gif'), (PIXEL, PIXEL)),
'!C.' : pygame.transform.scale(pygame.image.load('Clyde.gif'), (PIXEL, PIXEL)),
}
#Class stuff
class Ghost:
def __init__(self, ghostPixelX, ghostPixelY, sprite):
print("Init " + sprite)
self.ghostPixelX = ghostPixelX
self.ghostPixelY = ghostPixelY
self.sprite = sprite
def draw(self):
#print("draw " + self.sprite)
dis.blit(char_to_image(self.sprite), (self.ghostPixelX, self.ghostPixelY))
def erase(self):
#print("erase " + self.sprite)
# Erase Ghost by drawing black rectangle over it
pygame.draw.rect(dis, BLACK, (self.ghostPixelX, self.ghostPixelY, PIXEL, PIXEL))
boardX = int(self.ghostPixelX/PIXEL)
boardY = int(self.ghostPixelY/PIXEL)
# If the space contains food, redraw the food
if "." in board(boardY)(boardX):
dis.blit(char_to_image("."), (self.ghostPixelX, self.ghostPixelY))
def move(self, pacManPixelX, pacManPixelY):
#print("PreMove: " + str(self.sprite) + " " + str(self.ghostPixelX) + " " + str(self.ghostPixelY))
#if score moves, so does directions
#Sorts directions to which direction is best to take
directions = (NORTH, EAST, SOUTH, WEST)
score = ("","","","") #Which move is best
#Calculate distance between Ghost and PacMan
pixelDistanceX = pacManPixelX - self.ghostPixelX
pixelDistanceY = pacManPixelY - self.ghostPixelY
pixelDistance = maths.sqrt(pixelDistanceX**2 + pixelDistanceY**2)
#Calculate distance between Ghost and PacMan after a move in each direction
for i, direction in enumerate(directions):
ghostDX, ghostDY = direction
newGhostPixelX = self.ghostPixelX + ghostDX
newGhostPixelY = self.ghostPixelY + ghostDY
newPixelDistanceX = pacManPixelX - newGhostPixelX
newPixelDistanceY = pacManPixelY - newGhostPixelY
newPixelDistance = maths.sqrt(newPixelDistanceX**2 + newPixelDistanceY**2)
#Store how much better (closer) or worse (further away) the move would take the ghost from PacMan
score(i) = pixelDistance - newPixelDistance
#Insertion sort O(n)
#Iterates through the list for the next number to sort (start at pos 1)
for index in range(1, len(score)):
currentEntry = score(index)
currentEntryDir = directions(index)
position = index
#Iterates through the list for the number to swap
while position > 0 and score(position-1) > currentEntry:
#Copies the lower position into the original position, overwriting it
score(position) = score(position-1)
directions(position) = directions(position-1)
position = position - 1
#puts the stored value from position, into the final lower position
score(position) = currentEntry
directions(position) = currentEntryDir
# Take the now sorted list of moves, trying each one in turn and take the best move possible
for direction in reversed(directions):
ghostDX, ghostDY = direction
newGhostPixelX = self.ghostPixelX + ghostDX
newGhostPixelY = self.ghostPixelY + ghostDY
# Ghosts cant hyperjump
if newGhostPixelX >= 0 and newGhostPixelX < BOARDPIXELWIDTH and newGhostPixelY >= 0 and newGhostPixelX < BOARDPIXELHEIGHT:
# Ghosts can't go through walls
if TestMove(newGhostPixelX, newGhostPixelY, HYPERJUMPNOTALLOWED):
#print(direction)
self.ghostPixelX = newGhostPixelX
self.ghostPixelY = newGhostPixelY
#print("PostMove: " + str(self.sprite) + " " + str(self.ghostPixelX) + " " + str(self.ghostPixelY))
print("")
return
#Functions
# Load Board from a file in current directory
# Boards are text files called "board-X.txt"
def LoadBoard():
#ToDo load board from file
#10 x 10 Board
board = (('=BR', '=RL', '=RL', '=L', 'O', '.', '=R', '=RL', '=RL', '=BL'),
('=TB', '!B.', '.', '.', '.', '.', '.', '.', '!I.', '=TB'),
('=TB', '.', '=BR', '=L', '.', '.', '=R', '=BL', '.', '=TB'),
('=T', '.', '=T', '.', '.', '.', '.', '=T', '.', '=T'),
('.', '.', '.', '.', '.', 'U', '.', '.', '.', 'O'),
('O', '.', '.', '.', '.', '.', '.', '.', '.', '.'),
('=B', '.', '=B', '.', '.', '.', '.', '=B', '.', '=B'),
('=TB', '.', '=TR', '=L', '.', '.', '=R', '=TL', '.', '=TB'),
('=TB', '!C.', '.', '.', '.', '.', '.', '.', '!P.', '=TB'),
('=TR', '=RL', '=RL', '=L', '.', 'O', '=R', '=RL', '=RL', '=TL'))
global foodTotal
global pacManPixelX, pacManPixelY, pacManFacing, pacManDX, pacManDY
global Pinky, Blinky, Inky, Clyde
foodTotal = 0
pacManPixelX = pacManPixelY = pacManDX = pacManDY = 0
pacManFacing = UP
#ToDo Load Board Pixel Width and Height here and delete from top of this file
for boardY, line in enumerate(board):
for boardX, symbol in enumerate(line):
if symbol == ".":
foodTotal +=1 # Count how much food we start with
elif symbol == "!P." or symbol == "!P": #Which Ghost is it?
Pinky = Ghost(boardX * PIXEL, boardY * PIXEL, "!P") #Create the ghost!
elif symbol == "!B." or symbol == "!B":
Blinky = Ghost(boardX * PIXEL, boardY * PIXEL, "!B")
elif symbol == "!I." or symbol == "!I":
Inky = Ghost(boardX * PIXEL, boardY * PIXEL, "!I")
elif symbol == "!C." or symbol == "!C":
Clyde = Ghost(boardX * PIXEL, boardY * PIXEL, "!C")
elif symbol == "U":
pacManPixelX = boardX * PIXEL # Get PacMan starting position
pacManPixelY = boardY * PIXEL
return board
#Draw Board
def DrawBoard():
for y, line in enumerate(board):
# Convert from board PIXEL to real PIXEL
y *= PIXEL
for x, symbol in enumerate(line):
# Convert from board PIXEL to real PIXEL
x *= PIXEL
# Convert board chars to gif filename using dictionary
if symbol != "O":
dis.blit(char_to_image(symbol), (x, y))
#Test if Character can move to new location
def TestMove(newPixelX, newPixelY, hyperJumpAllowed):
#TODO This is used for Ghosts and PacMan, Ghosts are not allowed to move in to a square already occupied by a Ghost
# Pacman is, but then will die
if newPixelX >= BOARDPIXELWIDTH or newPixelY >= BOARDPIXELHEIGHT or newPixelX < 0 or newPixelY < 0:
if (hyperJumpAllowed):
#If move would be a HyperJump, and HypeJumps are allowed then move must be ok
return True
else:
#If move would be a HyperJump, and HypeJumps are not allowed then move must not be ok
return False
newBoardX = int(newPixelX/PIXEL)
newBoardY = int(newPixelY/PIXEL)
#Test if move would end up in a wall
if "=" in board(newBoardY)(newBoardX):
return False
else:
return True
#Move PacMan to new location, but dont draw the update
def MovePacMan(pixelX, pixelY, dPixelX, dPixelY, facing):
# Move PacMan
newPixelX = pixelX + dPixelX
newPixelY = pixelY + dPixelY
# Check if move needs to be a HyperJump and if so HyperJump
if (newPixelX >= BOARDPIXELWIDTH):
newPixelX = 0
elif (newPixelX < 0):
newPixelX = BOARDPIXELWIDTH - PIXEL
if (newPixelY >= BOARDPIXELHEIGHT):
newPixelY = 0
elif (newPixelY < 0):
newPixelY = BOARDPIXELHEIGHT - PIXEL
return newPixelX, newPixelY
def moveGhosts(pacManPixelX, pacManPixelY):
Pinky.move(pacManPixelX, pacManPixelY)
Blinky.move(pacManPixelX, pacManPixelY)
Inky.move(pacManPixelX, pacManPixelY)
Clyde.move(pacManPixelX, pacManPixelY)
def eraseGhosts():
Pinky.erase()
Blinky.erase()
Inky.erase()
Clyde.erase()
def ErasePacMan(pixelX, pixelY):
# Erase PacMan from old position by drawing black rectangle over it
pygame.draw.rect(dis, BLACK, (pixelX, pixelY, PIXEL, PIXEL))
def drawGhosts():
Pinky.draw()
Blinky.draw()
Inky.draw()
Clyde.draw()
#Draw PacMan at a new position
def DrawPacMan(pixelX, pixelY, facing):
# Draw PacMan at new position
if facing == UP:
dis.blit(char_to_image('U'), (pixelX, pixelY))
elif facing == DOWN:
dis.blit(char_to_image('D'), (pixelX, pixelY))
elif facing == LEFT:
dis.blit(char_to_image('L'), (pixelX, pixelY))
elif facing == RIGHT:
dis.blit(char_to_image('R'), (pixelX, pixelY))
# Remove food at new board position
board(int(pixelY / PIXEL))(int(pixelX / PIXEL)) = "O"
#Play sounds as PacMan eats
def PlaySound(pixelX, pixelY):
boardX = int(pixelX / PIXEL)
boardY = int(pixelY / PIXEL)
#Play sound if new position has food
if board(boardY)(boardX) == ".":
# Alternate between two different sounds
if (boardX + boardY) % 2 == 0:
food1Sound.play()
else:
food2Sound.play()
else:
defaultSound.play()
def message(msg, color, pixelX, pixelY, fontSize, align):
#Setup font
font_style = pygame.font.SysFont("bahnschrift", fontSize)
# Render text ont a surface
msgRendered = font_style.render(msg, True, color)
# Get size of surface
msgPixelWidth, msgPixelHeight = msgRendered.get_size()
# Change position to draw in relation to align
if align == CENTRE_MID:
pixelX = pixelX - (msgPixelWidth / 2)
pixelY = pixelY - (msgPixelHeight / 2)
elif align == CENTRE_TOP:
pixelX = pixelX - (msgPixelWidth / 2)
dis.blit(msgRendered, (pixelX, pixelY))
#Main Code
pygame.init()
#Setup display and pygame clock
dis = pygame.display.set_mode((BOARDPIXELWIDTH, BOARDPIXELHEIGHT + ( 2 * PIXEL)))
pygame.display.set_caption('Pac-man by ME')
clock = pygame.time.Clock()
#Setup Sounds
if os.path.isfile("1-pellet1.wav") and os.path.isfile("1-pellet2.wav") and os.path.isfile("1-default.wav"):
sound = True
food1Sound = pygame.mixer.Sound("1-pellet1.wav")
food2Sound = pygame.mixer.Sound("1-pellet2.wav")
defaultSound = pygame.mixer.Sound("1-default.wav")
else:
print("Warning: Sound files not found, not playing sounds.")
sound = False
#Load board from file
#ToDo Load random board or different board each level
board = LoadBoard()
#Draw Board
DrawBoard()
pygame.display.flip()
#Game Loop
while not gameOver:
for event in pygame.event.get():
#Allows quitting
if event.type == pygame.QUIT:
gameOver = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
pacManFacing = LEFT
pacManDX, pacManDY = WEST
#pacManDX = -PIXEL
#pacManDY = 0
elif event.key == pygame.K_RIGHT:
pacManFacing = RIGHT
pacManDX, pacManDY = EAST
#pacManDX = PIXEL
#pacManDY = 0
elif event.key == pygame.K_UP:
pacManFacing = UP
pacManDX, pacManDY = NORTH
#pacManDY = -PIXEL
#pacManDX = 0
elif event.key == pygame.K_DOWN:
pacManFacing = DOWN
pacManDX, pacManDY = SOUTH
#pacManDY = PIXEL
#pacManDX = 0
#Can we move to new position?
if TestMove(pacManPixelX + pacManDX, pacManPixelY + pacManDY, HYPERJUMPALLOWED):
#Erase PacMan
ErasePacMan(pacManPixelX, pacManPixelY)
#Calculate new position
pacManPixelX, pacManPixelY = MovePacMan(pacManPixelX, pacManPixelY, pacManDX, pacManDY, pacManFacing)
#print("pacManPixelX " + str(pacManPixelX) + " pacManPixelY " + str(pacManPixelY))
if board(int(pacManPixelY / PIXEL))(int(pacManPixelX / PIXEL)) == ".":
score+=1
foodTotal-=1
#Sound
if sound:
PlaySound(pacManPixelX, pacManPixelY)
#Draw the turn and remove food
DrawPacMan(pacManPixelX, pacManPixelY, pacManFacing)
#Update the score
pygame.draw.rect(dis, BLACK, (0, BOARDPIXELHEIGHT, BOARDPIXELWIDTH, PIXEL))
message(("You're score is " +str(score)), RED, 0, (BOARDPIXELHEIGHT), 15, LEFT_TOP)
# Ghosts
eraseGhosts()
#Calculate new Ghost position
moveGhosts(pacManPixelX, pacManPixelY)
#Draw new Ghost positions on the screen
drawGhosts()
pygame.display.update()
#TODO Has the ghost caughtPacMan, if so pacman looses 1 of 3 lives.
# So need lives system - 3 pacmen bottom right of screen that get 'used up' each time one dies
# What happens when Pacman dies? Ghosts get reset, pacman gets reset, score -10 and then carry on?
# Hint, pac man moves first, so when each ghost moves you can test if it has hit pacman
#if ghostPixelX == pacManPixelX and ghostPixelY == pacManPixelY:
# gameOver = True
#Win
if foodTotal == 0:
gameOver = True
win = True
#Tick the clock
clock.tick(FRAMERATE)
if win == True:
pygame.draw.rect(dis, YELLOW, (0, 0, BOARDPIXELWIDTH, BOARDPIXELHEIGHT))
message(("You Win!"), RED, BOARDPIXELWIDTH / 2, BOARDPIXELHEIGHT / 2, 15, CENTRE_MID)
message(("This message will dissapear in 5 seconds"), RED, (BOARDPIXELWIDTH / 2), (BOARDPIXELHEIGHT / 2 + PIXEL), 10, CENTRE_TOP)
pygame.display.update()
time.sleep(5)
else:
pygame.draw.rect(dis, RED, (0, 0, BOARDPIXELWIDTH, BOARDPIXELHEIGHT))
message(("You Lose!"), YELLOW, BOARDPIXELWIDTH / 2, BOARDPIXELHEIGHT / 2, 15, CENTRE_MID)
message(("This message will dissapear in 5 seconds"), YELLOW, (BOARDPIXELWIDTH / 2), (BOARDPIXELHEIGHT / 2 + PIXEL), 10, CENTRE_TOP)
pygame.display.update()
time.sleep(5)
pygame.quit()
quit()
Thanks very much