9
\$\begingroup\$

I have been coding 'Pac-Man' with my 16yo. A hopefully not too 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 from perfect, and especially not in Python. Which was only just being invented when I was learning this stuff. 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 in the future

Clearly he has a long way to go. So looking for minor steps forwards that we can understand and work with rather than 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.

Code provided below. Or as a zip with the textures etc. Currently the code 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()
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Can whoever voted to close this question please leave a comment explaining why? \$\endgroup\$
    – Mast
    Apr 7, 2021 at 9:05
  • \$\begingroup\$ I did that, since, if I understand the question correctly, the OP asked us to review code of a third party, namely their son. \$\endgroup\$ Apr 12, 2021 at 10:57

2 Answers 2

4
\$\begingroup\$

I am looking for feedback on how to improve the professionalism of his code

how to be more 'pythonic'

The style you've used is atypical in Python as your code doesn't follow all of PEP 8. For example you've used camelCase rather than snake_case for function names and variables. Personally if your son wants to be a programmer, not a Python programmer, than ignoring the Python specific parts is fine. However your code has some inconsistencies, which would be better to iron out early to prevent becoming bad habits.

For example; CENTRE_MID vs HYPERJUMPALLOWED, moveGhosts vs MovePacMan, using " or ' to delimit strings, using spaces after commas - (0, 0, 255) and (255,0,0), always using equal spaces around operators x *= PIXEL or foodTotal +=1.

I'd recommend installing a linter to check your code. Or you could use a hinter like Black, yapf or autopep8.


  • char_to_image = {'.' : pygame.transform.scale(pygame.image.load('pellet.gif'), (PIXEL, PIXEL)),
                     '=' : pygame.transform.scale(pygame.image.load('wall-nub.gif'), (PIXEL, PIXEL)),
                     ...
                     }
    

    We can see you've got a fair amount of duplicate code here. I'd build a dictionary of strings and then build the dictionary of PyGame sprites afterwards.

    _CHAR_TO_IMAGE = {
        ".": "pellet.gif",
        "=": "wall-nub.gif",
    }
    
    char_to_image = {}
    for key, value in _CHAR_TO_IMAGE.items():
        char_to_image[key] = pygame.transform.scale(pygame.image.load(value), (PIXEL, PIXEL))
    

    We can instead use a dictionary comprehension to build the dictionary in a Pythonic manner. Dictionary comprehensions are like list comprehensions however we'd be building a dictionary rather than a list. Converting the for loop into a dictionary comprehension would look like:

    char_to_image = {
        key: pygame.transform.scale(pygame.image.load(value), (PIXEL, PIXEL))
        for key, value in _CHAR_TO_IMAGE.items()
    }
    
  • You should move your main code into a main function. You can then use a __main__ guard to only run code if the file is the entry-point to the program.

    Like main() in C-like programming languages.

  • if win == True:
    

    Since bool is a subclass of int true is equal to 1, 1.0 and 1+0j. You should use one of:

    • Check the type is 'truthy', so if win: is true. The code will match far more values for example 2 is truthy. So is [1], {1: 1} and {1}.

    • If you want to ensure the value is True you should use is. Needing to compare to just True is very rare.

    The recommended way to check the value is truthy.

  • if "=" in board[newBoardY][newBoardX]:
        return False
    else:
        return True
    

    We can just return the expression in the if.

    return "=" not in board[newBoardY][newBoardX]
    

    Since using truthy checks is the norm in Python; even if in doesn't return True or False any code calling the function would still work the same way. (Note: in always returns True or False however some other operations can return truthy but not True values.)

  • Ghost.move can be made a little simpler.

    1. pixelDistance isn't needed as using Pythagoras' theorem already gives you the hypotenuse (distance) between the two objects.

    2. We can use list.sort to sort the list.

      1. The sorting code will run in C not Python. (Probably will be faster)
      2. You don't have to write a sorting algorithm. (Makes the code simpler)
      3. Python uses the Timsort; an optimized version of the insertion sort. (Probably will be faster)
    3. # 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):
      

      Seems to me TestMove should include the first if statement. Since we've passed HYPERJUMPNOTALLOWED to the function and your comment says the code is preventing hyper-jumping.

  • I think you need to add a board class.

    • The board should convert (internally) from a Python list to pixels on the screen.
    • The board should allow consumers to interact with a simple 2D array so the consumer never touches pixels.
    • The board should have some convenience functions like checking if a place is a valid place to move.
    • The board should allow the consumer to move objects on the board.

    I think having a solid board class is the major pain point in your code.

    After building a standardized board you should build a game class. The game class should contain all the globals but should not include the event or blit loops. Calling the game class from such loops is ok.

#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 = {'.' : 'pellet.gif',
                  '=' : 'wall-nub.gif', 
                  '=T' : 'wall-end-b.gif',
                  '=R' : 'wall-end-l.gif',
                  '=L' : 'wall-end-r.gif',
                  '=B' : 'wall-end-t.gif',
                  '=TR' : 'wall-corner-ll.gif',
                  '=TL' : 'wall-corner-lr.gif',
                  '=BR' : 'wall-corner-ul.gif',
                  '=BL' : 'wall-corner-ur.gif',
                  '=TB' : 'wall-straight-vert.gif',
                  '=RL' : 'wall-straight-horiz.gif',
                  '=LTR' : 'wall-t-bottom.gif',
                  '=TRB' : 'wall-t-left.gif',
                  '=BLT' : 'wall-t-right.gif',
                  '=RBL' : 'wall-t-top.gif',
                  '=TRLB' : 'wall-x.gif',
                  'U' : 'pacman-u 4.gif',
                  'R' : 'pacman-r 4.gif',
                  'L' : 'pacman-l 4.gif',
                  'D' : 'pacman-d 4.gif',
                  '!P' : 'Pinky.gif',
                  '!P.' : 'Pinky.gif',
                  '!B' : 'Blinky.gif',
                  '!B.' : 'Blinky.gif',
                  '!I' : 'Inky.gif',
                  '!I.' : 'Inky.gif',
                  '!C' : 'Clyde.gif',
                  '!C.' : 'Clyde.gif',
                  }

char_to_image = {
    key: pygame.transform.scale(pygame.image.load(value), (PIXEL, PIXEL))
    for key, value in _CHAR_TO_IMAGE.items()
}


#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):
        directions = [NORTH, EAST, SOUTH, WEST]
        scored_directions = [
            (
                maths.sqrt(
                    (pacManPixelX - self.ghostPixelX - ghostDX) ** 2
                    + (pacManPixelY - self.ghostPixelY - ghostDY) ** 2
                ),
                (ghostDX, ghostDY)
            )
            for (ghostDX, ghostDY) in directions
        ]
        scored_directions.sort()
        for _, (ghostDX, ghostDY) in scored_direction:
            newGhostPixelX = self.ghostPixelX + ghostDX
            newGhostPixelY = self.ghostPixelY + ghostDY

            if TestMove(newGhostPixelX, newGhostPixelY, HYPERJUMPNOTALLOWED):
                self.ghostPixelX = newGhostPixelX
                self.ghostPixelY = newGhostPixelY
                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:
        return hyperJumpAllowed
    
    newBoardX = int(newPixelX/PIXEL)
    newBoardY = int(newPixelY/PIXEL)
  
    #Test if move would end up in a wall    
    return "=" not in board[newBoardY][newBoardX]


#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"


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


#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 main():
    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:
        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()


if __name__ == "__main__":
    main()
\$\endgroup\$
7
  • 1
    \$\begingroup\$ Your fourth point is buggy, you actually want return "=" not in board[newBoardY][newBoardX], at least that's what the original if-statement does. \$\endgroup\$ Apr 6, 2021 at 11:42
  • \$\begingroup\$ even if in doesn't return True or False: Do membership test using in not return True / False? >>> ("h" in "hello") is True returns True for me (Python 3.9.1). \$\endgroup\$ Apr 6, 2021 at 11:47
  • 1
    \$\begingroup\$ @riskypenguin Thanks! I've edited to fix the in rather not in. No __contains__ is always converted to a bool. However I'm intending to use the specific to talk about the generic - like if you have if a: return True else: return False -> return a. a isn't always guaranteed to be True. Can you double check my latest edits have fixed both problems? \$\endgroup\$
    – Peilonrayz
    Apr 6, 2021 at 12:07
  • \$\begingroup\$ Problem solved and confusion cleared up! =) \$\endgroup\$ Apr 6, 2021 at 12:14
  • \$\begingroup\$ Thankyou very much for your comments @Peilonrayz. Totally agree about a Board class - I fall over Pixels vs Board position all the time. I am struggling to understand the new Ghosts.move function though. I understand that the separate Directions and Score that I had are not good (as they are not linked), but in your code you do something very different that I don't understand. Do you create a Tuple of the two values? Are you missing a definition of directions (to use in "for direction in directions")? Why does the for x in x go at the bottom pf the for loop? Thanks \$\endgroup\$
    – Kevin W
    Apr 6, 2021 at 20:52
1
\$\begingroup\$

it's a nice thing to teach your kid programming langage!

I have several advises, ideas :

1: why use gif while those aren't animated, you could as well use png for those color styles.

2: second for your level layout, you can just store 1 value for wall and let decide the graphic render which part of your wall to draw (based on neighbooring cells), could even be used later if you decide to modifiy level structure ingame.

3: even if your game is grid based, you could add smooth movement by translation (bot gosths and pacman)

4: in the v10 i tested, noting happen when i got it by ghost

5: for more oop, you can create a Level classe which load your level and when you win 1 (i couldn't) load the next one.

6: you can also probably tweak framerate so you gain more granularity, allowing to change direction in mid course (not sure if you'll get what i mean)

7: adding menu, GUI, pause...

I didn't played the original pacman, but tried http://www.jeux.org/jeu/pacman-2.html, you could see some thing you can improve

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.