JustToThePoint English Website Version
JustToThePoint en español
JustToThePoint in Thai

Programming a Pac-Man in Python II

Pacman eats pellets because he has to, fruit because he should, and ghosts because they remind him that death is the only true escape from his hellish maze, Connor Choadsworth

Ghosts

Let’s implement a class for the game’s four ghosts or monsters: Inky, Blinky, Pinky, and Clyde. Coding Pac-Man with Python

import pygame
from pygame.locals import *
from vector import myVector
from settings import *
from entity import Entity
from sprites import GhostSprites
import copy

class Ghost(Entity):
    def __init__(self, node, maze, scatterHome, pacman, blinky=None):
        Entity._init__(self, node, maze)
        self.name = GHOST
        self.points = 200
        self.goal = myVector()
        self.pacman = pacman
        self.blinky = blinky
        self.homeNode = node # There are two main nodes: the ghosts' home and the scatter's home, the place where ghosts scatter around wandering relentlessly.
        self.scatterHome = scatterHome

    def __str__(self):
    """ This method is included for debugging purposes."""
        namesGhost = { 
            BLINKY: "Blinky", 
            PINKY: "Pinky", 
            INKY: "Inky", 
            CLYDE: "Clyde"}
        modeGhost = { 
            SCATTER: "Scatter", 
            CHASE: "Chase", 
            FREIGHT: "Freight", 
            DEATH: "Death",
            ZOMBIE: "Zombie"}
        
        return namesGhost[self.name] + " is " + modeGhost[self.mode]

When a ghost dies, it goes immediately back to his home: self.directionMethod = self.goalDirection and self.goal = self.homeNode.position. However, self.direction = STOP because it stops moving while its death animation is taking place.

    def die(self):
        """ It is called when Pac-Man collides with a ghost which is in FREIGHT mode."""
        self.mode = DEATH
        self.direction = STOP
        self.directionMethod = self.goalDirection
        self.goal = self.homeNode.position

    def update(self, dt):
        """ It updates the ghost's state. It calls the parent class method (Entity.update(self, dt)). If the ghost is in ZOMBIE mode and has reached the ghosts' home, its mode changes to CHASE."""
        self.sprites.update(dt)
        Entity.update(self, dt)
        if self.mode == ZOMBIE and self.node == self.homeNode:
            self.mode = CHASE

    def updateStatus(self, newMode):
        """ We will implement a GameControl class to manage the ghosts' modes and the game's pause state. 
        This class will iterate over the four ghosts (for ghost in self.ghosts) and call this method (ghost.updateStatus(newMode)) to make each ghost aware of his new status so each one of them can act accordingly."""
        self.mode = newMode
        if self.mode == SCATTER:
            self.scatter()
        elif self.mode == CHASE:
            self.chase()
        elif self.mode == FREIGHT:
            self.freight()
            
    def scatter(self):
        """ It implements the SCATTER mode. The ghost scatters around wandering relentlessly across its scatter home at normal speed."""
        self.mode = SCATTER
        self.sprites.death.finished = False
        self.setSpeed(NORMAL_SPEED)
        self.goal = self.scatterHome
        self.directionMethod = self.goalDirection

    def chase(self):
        """ It implements the CHASE mode, the ghosts' AI to chase PacMan.""" 
        self.setSpeed(NORMAL_SPEED)
        if GAME_MODE == STUPID_MODE:
        # The randomDirection is the method to select a direction in STUPID_MODE. It just picks a random direction.
            self.directionMethod = self.randomDirection.
        # If the game is played in CLASSIC_MODE (it is defined in settings.py), it selects the direction that minimizes the distance to a goal (self.directionMethod = self.goalDirection). Typically, this goal will be PacMan's position (self.goal = self.pacman.position). 
        elif GAME_MODE == CLASSSIC_MODE: 
            if self.name==BLINKY:
                self.goal = self.pacman.position
            # If the ghost is PINKY, the goal will be four tiles ahead of PacMan.
            elif self.name==PINKY:
                self.goal = self.pacman.position + self.pacman.directions[self.pacman.direction] * TILEWIDTH * 4
            elif self.name==INKY:
            # If the ghost is INKY, it calculates the position that is two tiles in front of Pacman, then subtracts from it Blinky's position, and finally, it multiplies the result by 2.
                vec1 = self.pacman.position + self.pacman.directions[self.pacman.direction] * TILEWIDTH * 2
                vec2 = (vec1 - self.blinky.position) * 2
                self.goal = self.blinky.position + vec2
                self.scatterHome = myVector(TILEWIDTH*NCOLS, TILEHEIGHT*NROWS)
            elif self.name==CLYDE:
            # If the ghost is CLYDE and he comes within eight tiles of the circular hero, he will retreat or hide away. Whenever Clyde is more than 8 tiles away from Pac-Man, his movements are identical to Pinky. It will actively attempt to chase Pac-Man by moving towards four tiles ahead of him.
                d = self.pacman.position - self.position
                if d.magnitude() <= (TILEWIDTH * 8)**2:
                    self.scatter()
                else:
                    self.goal = self.pacman.position + self.pacman.directions[self.pacman.direction] * TILEWIDTH * 4                

                self.scatterHome = myVector(0, TILEHEIGHT*NROWS)

            self.directionMethod = self.goalDirection
        elif GAME_MODE == ADVANCED_MODE:
        # If the game is played in ADVANCED_MODE (it is defined in settings.py), it selects the direction that minimizes the "real" distance to Pac-Man because **it actually uses A* to find the path to our circular hero in the game's maze**.
            self.directionMethod = self.astar

    # When Pacman eats a power pellet, then he can gobble the game's ghosts for a short time. During the "FREIGHT" mode, the game's ghosts move around randomly (self.directionMethod = self.randomDirection) and at a slower speed (self.setSpeed(LOW_SPEED).  
    def freight(self):
        self.setSpeed(LOW_SPEED)
        self.directionMethod = self.randomDirection         
    
    # It resets the ghost to its NORMAL state.    
    def reset(self):
        Entity.reset(self)
        self.points = 200
        self.direction = LEFT
        self.scatter()
    
    # After being eaten by Pac-Man, the ghost changes to ZOMBIE mode. During this mode, it moves to its home at "super" speed.
    def revive(self):
        self.mode = ZOMBIE
        self.direction = LEFT
        self.setSpeed(SUPER_SPEED)
        self.goal = self.homeNode.position
        self.directionMethod = self.goalDirection

class Blinky(Ghost):
    def __init__(self, node, maze, scatterHome, pacman, blinky=None):
        Ghost._init__(self, node, maze, scatterHome, pacman, blinky=None)
        self.name = BLINKY
        self.sprites = GhostSprites(self)

class Pinky(Ghost):
    def __init__(self, node, maze, scatterHome, pacman, blinky):
        Ghost._init__(self, node, maze, scatterHome, pacman, blinky)
        self.name = PINKY
        self.sprites = GhostSprites(self)   

class Inky(Ghost):
    def __init__(self, node, maze, scatterHome, pacman, blinky):
        Ghost._init__(self, node, maze, scatterHome, pacman, blinky)
        self.name = INKY
        self.sprites = GhostSprites(self)

class Clyde(Ghost):
    def __init__(self, node, maze, scatterHome, pacman, blinky):
        Ghost._init__(self, node, maze, scatterHome, pacman, blinky)
        self.name = CLYDE
        self.sprites = GhostSprites(self)

class Ghosts(object):
    # It implements a list with all the game's ghosts.
    def __init__(self, node, maze, pacman):
        self.blinky = Blinky(node, maze, myVector(), pacman)
        self.pinky = Pinky(node, maze, myVector(TILEWIDTH*NCOLS, 0), pacman, self.blinky)
        self.inky = Inky(node, maze, myVector(300, 0), pacman, self.blinky)
        self.clyde = Clyde(node, maze, myVector(TILEWIDTH*NCOLS-300, 0), pacman, self.blinky)
        self.ghosts = [self.blinky, self.pinky, self.inky, self.clyde]

    # We can use the iterator of the built-in "list" so we are able to iterate over the ghosts easily.
    def __iter__(self):
        return iter(self.ghosts)

    # When Pac-Man eats his first ghost, he earns 200 points. When he eats the second ghost, he gets 400 points, then 800, and finally, 1600. In other words, the worth of each ghost doubles as Pac-Man keeps eating all his enemies. It is called in the Game class when Pac-Man collides with a ghost (if self.pacman.collideGhost(ghost):) that is in FREIGHT mode (if ghost.mode == FREIGHT:)
    def updatePoints(self):
        for ghost in self:
            ghost.points *= 2

    # When Pac-Man eats a new power pellet, the ghosts' value is reset to 200. 
    def resetPoints(self):
        for ghost in self:
            ghost.points = 200

    def reset(self):
        for ghost in self:
            ghost.reset()

    # It hides all the game's ghosts.
    def hide(self):
        for ghost in self:
            ghost.visible = False

    def show(self):
        for ghost in self:
            ghost.visible = True

    # It renders the ghosts. It is called from the game's render() method: self.ghosts.render(self.screen)
    def render(self, screen):
        for ghost in self:
            ghost.render(screen)

    # It updates all the ghosts. It is called from the game's update() method: self.ghosts.update(dt)
    def update(self, dt):
        for ghost in self:
            ghost.update(dt)

Programming a Pac-Man clone

Pellet

PacMan is basically a maze game where Pac-Man eats pellets while being chased by four ghosts. Pac-Man can eat fruits, pellets, and power pellets, be chased and killed by ghosts and eat them. Let’s create a class to implement these pellets.

There are four power pellets. They allow Pac-Man to fight back and eat the ghosts.

All pellets have a name and a position. They will be drawn as white circles with a radius of int(2 * TILEWIDTH / 16) or int(8 * TILEWIDTH / 16) (the power pellet is obviously bigger). self.point specifies how many points a pellet is worth: 10 or 50 points. self.visible allows us to hide a pellet. In other words, we only render it if it is visible.

import pygame
from vector import myVector
from settings import *
import numpy as np

class Pellet(object):
    """ This class implements a pellet."""
    def __init__(self, row, column, power=False):
        self.name = PELLET
        self.position = myVector(column*TILEWIDTH, row*TILEHEIGHT)
        self.flashTime = 0.2
        self.timer = 0
        self.collideRadius = int(2 * TILEWIDTH / 16)
        self.visible = True
        self.power = power # This attribute is a flag to indicate if the object is a power or a normal pellet.
        if self.power:
            self.points = 50
            self.radius = int(8 * TILEWIDTH / 16)
        else:
            self.points = 10
            self.radius = int(2 * TILEWIDTH / 16)
               
    def isPowerPellet(self):
        return self.power

    def adjustPosition(self):
    # It adjusts the pellet's position so it can be drawn correctly on the maze.
        adjustPosition =  self.position + myVector(TILEWIDTH, TILEHEIGHT) / 2
        return adjustPosition.getVectorAsInt()

    def render(self, screen):
    # It renders or draws the pellet as a white circle of self.radius if it is visible. This attribute will allow us to create the effect that the pellet is flashing on and off.
        if self.visible:
            pygame.draw.circle(screen, WHITE, self.adjustPosition(), self.radius)
     
    def update(self, dt):
    # It updates the pellet. This is how we create the effect that the pellet is flashing on and off by using a timer (self.timer) that resets itself every self.flashTime (0.2) and flips self.visible.
        self.timer += dt
        if self.timer >= self.flashTime:
            self.visible = not self.visible
            self.timer = 0
   
class Pellets(object):
   # It implements a list with all the game's pellets. 
    def __init__(self, pelletfile):
        self.myPellets = []
        self.createPelletList(pelletfile)
        self.numEaten = 0

    def __iter__(self):
        # It allows us to iterate over the game's remaining pellets with an intuitive syntax: for pellet in self.
        return iter(self.myPellets)

    def update(self, dt):
        # It updates only the power pellets. 
        for pellet in self:
            if pellet.isPowerPellet():
                pellet.update(dt)

    def createPelletList(self, pelletfile):
        # It generates all our pellets from the maze file.
        with open(pelletfile) as f:
            lines = f.readlines()

        lines = [line.replace(' ', '') for line in lines]

        for row, line in enumerate(lines):
            for col, character in enumerate(line):
                # If there is a dot "." or a plus "+" (there is also a node), we add a pellet to our list.
                if character in ['.', '+']:
                    self.myPellets.append(Pellet(row, col))
                # If there is a "P" (there is also a node) or a "p", we add a power pellet to our list.
                elif character in ['P', 'p']:
                    self.myPellets.append(Pellet(row, col, True))
    
    def isEmpty(self):
        # It is used by the Game's class to determine if the level is finished or not. When Pac-Man eats all the maze's pellets, the player advances to the next level. The game ends when all Pac-Man's lives are lost. 
        return len(self.myPellets) == 0
    
    def render(self, screen):
        # It renders the remaining pellets on the screen.
        for pellet in self:
            pellet.render(screen)

    def removePellet(self, pellet):
        # It removes a pellet from myPellets because Pac-Man has already eaten it.
        self.numEaten += 1
        self.myPellets.remove(pellet)

A Game controller

A game controller class is going to be implemented to be able to pause the game and control the ghosts’ state.

from settings import *

class GameControl():
    def __init__(self, ghosts):
        self.timer = 0
        self.ghosts = ghosts
        self.scatter()
        self.paused = True
        self.pauseTime = None
        self.func = None
        self.playerPaused = False
        self.animationInProgress = False
    
    def flip(self):
        self.paused = not self.paused

When Pac-Man or a ghost dies, the game object will call setAnimationInProgress so we can pause the game for a few seconds to show the death’s animation: self.control.setAnimationInProgress(True).

# The game class' update method... 
[...] 
    afterPauseMethod = self.control.update(dt) # self.control.update(dt) returns a method if the animation is finished. 
    if afterPauseMethod is not None:     
        afterPauseMethod() [...]
    def setAnimationInProgress(self, value):
        self.animationInProgress = value

There are four reasons to pause the game:

  1. The game is paused when we start o reset a new level (nextLevel, resetLevel) or restart the game: self.control.setPause(playerPaused=False)
  2. It is paused when a ghost dies: self.control.setAnimationInProgress(True) and self.control.setPause(playerPaused = False, pauseTime=SMALL_PAUSE, func=self.showEntities). When a ghost or Pac-Man dies, a pauseTime is set.
  3. The game is paused when Pac-Man dies: self.control.setAnimationInProgress(True) and self.control.setPause(playerPaused = False, pauseTime=BIG_PAUSE, func=self.restartGame -Pac-Man’s lives = 0- or func=self.resetLevel )
  4. The user wants to pause the game and presses the space bar: paused = self.control.setPause(playerPaused=True) (This code is situated in the game’s method checkEvents().)
     def setPause(self, playerPaused=False, pauseTime=None, func=None):
        # It resets the timer. If the player has pressed the spacebar, it flips the pause attribute.
        self.timer = 0
        self.func = func
        self.pauseTime = pauseTime
        self.playerPaused = playerPaused
        if self.playerPaused:
            self.flip()
        else:
            self.paused = True

        return self.paused

    def scatter(self):
        # The Game is in SCATTER mode for SCATTER_TIME seconds. It updates all the ghosts with their new mode or status.
        self.mode = SCATTER
        self.time = SCATTER_TIME
        self.timer = 0
        self.updateGhosts(SCATTER)

    def chase(self):
        # The Game is in CHASE mode for CHASE_TIME seconds. It updates all the ghosts with their new mode or status.
        self.mode = CHASE
        self.time = CHASE_TIME
        self.timer = 0
        self.updateGhosts(CHASE)

    def update(self, dt):
        # It is called by our game's update method.
        self.timer += dt
        # If the game is paused and a pauseTime has been set (PacMan or a ghost has just died), we update the timer and check if this paused (predefined) time has already been reached. If so, we return the function to be executed after this time (that has allowed the death's animation to take place) is over. Otherwise, we return None. If the game is paused and a pauseTime has not been set, there is nothing to be done. We are just waiting for the user to press the spacebar.
        if self.paused == True:
            if self.pauseTime is not None:
                if self.timer >= self.pauseTime:
                    self.timer = 0
                    self.paused = False
                    self.pauseTime = None
                    self.animationInProgress = False
                    return self.func
        elif self.timer >= self.time:
        # If the game is not paused and the timer has already reached a predefined time (self.time), it swaps between SCATTER and CHASE or changes the game's mode from FREIGHT to CHASE.
            if self.mode is SCATTER:
                self.chase()
            elif self.mode is CHASE:
                self.scatter()
            elif self.mode is FREIGHT:
                self.chase()
                
        return None

    def updateGhosts(self, newMode):
        """ It iterates over the ghosts and calls their updateStatus() method to make them aware of their new mode."""
        for ghost in self.ghosts:
            ghost.updateStatus(newMode)

    def startFreight(self):
        """ It is called when Pac-Man eats a power pellet. It starts the FREIGHT MODE. Observe that the ghosts' points are reset to their initial values."""
        self.mode = FREIGHT
        self.time = FREIGHT_TIME
        self.timer = 0
        self.updateGhosts(FREIGHT)
        self.ghosts.resetPoints()

Fruits

The fruits in the original Pac-Man were: Cherry, Strawberry, Orange, Apple, Melon, Galaxian Starship, Bell, and Key. Pac-Man eats fruits and gets rewarded with points.

The fruits are created in the game’s level two times, i.e., when Pac-Man has already eaten FIRST_FRUIT or SECOND_FRUIT pellets, and they have a lifespan of LIFESPAN_FRUIT.

if self.pellets.numEaten == FIRST_FRUIT or self.pellets.numEaten == SECOND_FRUIT:     
    if self.fruit is None:         
        self.fruit = Fruit(self.myMaze.getNodeFromTiles(9, 20), self.level)
import pygame
from entity import Entity
from settings import *
from sprites import FruitSprites

class Fruit(Entity):
    def __init__(self, node, level):
        Entity._init__(self, node)
        self.name = FRUIT
        self.lifespan = self.lifespan = LIFESPAN_FRUIT
        self.timer = 0
        self.destroy = False
        self.points = 100*(level+1)
        self.setBetweenNodes(RIGHT)
        self.sprites = FruitSprites(self, level)

The method update() allows us to keep track of how much time it has been visible and self.destroy is set to True after self.timer >= self.lifespan.

self.fruit is set to None and, as a consequence, the object is “really” destroyed when:

    def update(self, dt):
        self.timer += dt
        if self.timer >= self.lifespan:
            self.destroy = True

Sprites

Sprites are images or lists of images that are assigned to objects. They represent game assets and are used in games to collectively create a scene.

They are used to create complex and visually exciting games and interesting animations that consumers demand. Pac-Man, the four ghosts, the game’s fruits, and even the maze itself are all drawn using sprites.

A sprite sheet is a collection of artworks, typically for the same character or game. In our case, we have all our sprites images in a single file. It’s faster and more efficient to load one image and just draw a portion of the image rather than loading different images.

import pygame
from settings import *
from animation import myAnimator

class Spritesheet(object):
    """ It implements our sprite sheet."""
    def __init__(self):
        self.sheet = pygame.image.load("spritesheet.png").convert()
        # We load the sprite sheet file and we call Surface.convert() with no arguments, to create a copy that will draw more quickly on the screen.
        transcolor = self.sheet.get_at((0,0))
        # It gets the color value at the left top corner of spritesheet.png.
        self.sheet.set_colorkey(transcolor)
        # It sets the current transparent color for the surface. When blitting this surface onto a destination, any pixels that have the same color as transcolor will be transparent. 
        width = int(self.sheet.get_width() / BASETILEWIDTH * TILEWIDTH)
        height = int(self.sheet.get_height() / BASETILEHEIGHT * TILEHEIGHT)
        self.sheet = pygame.transform.scale(self.sheet, (width, height))
        # It scales the image to fit our needed size (width, height)
        
    def getImage(self, x, y, width, height):
        """ It returns an image from our sprite sheet. It receives as parameters the image's position (x, y) on the sprite sheet and its size (width, height)."""
        x *= TILEWIDTH
        y *= TILEHEIGHT
        self.sheet.set_clip(pygame.Rect(x, y, width, height))
        # It sets the current clipping area of our sprite sheet surface.
        return self.sheet.subsurface(self.sheet.get_clip())
        # It grabs and returns the previously selected clipping area of our sprite sheet surface.

class PacmanSprites(Spritesheet):
    """ A class that contains all the sprites for Pac-Man. It inherits from Spritesheet. The Pacman class has an attribute that is an object of this class: _self.sprites = PacmanSprites(self)_"""
    def __init__(self, entity):
        Spritesheet._init__(self)
        self.entity = entity
        self.entity.image = self.getStartImage()       
        self.animations = {}
        self.defineAnimations()
        self.stopimage = (8, 10)

    def defineAnimations(self):
        """ It defines all Pac-Man's animations sprites."""
        self.animations[LEFT] = myAnimator(((8, 10), (0, 8), (0, 10), (0, 8)))
        # Pacman's left animation starts with his mouth completely closed (8, 10; it is just a yellow circle), then it moves to a picture with its mouth slightly opened (0, 8), next it is drawn with its mouth fully opened (0, 10); and finally, we go back to draw him with his mouth slightly opened before the cycle starts again.
        self.animations[RIGHT] = myAnimator(((10,10), (2, 8), (2, 10), (2, 8)))
        self.animations[UP] = myAnimator(((10,10), (6, 8), (6, 10), (6, 8)))
        self.animations[DOWN] = myAnimator(((8,10), (4, 8), (4, 10), (4, 8)))
        # Pacman's death sprites are at the bottom row of our sprite sheet, from (0, 12) to (20, 12).
        self.animations[DEATH] = myAnimator(((0, 12), (2, 12), (4, 12), (6, 12), (8, 12), (10, 12), (12, 12), (14, 12), (16, 12), (18, 12), (20, 12)), loop=False, superSpeed=True)

Pacman’s spritesheet

    def getStartImage(self):
        """ It returns the image we want our hero to begin the game with. It is located in column 8 and row 10 on the sprite sheet (it is just a yellow circle).""" 
        return self.getImage(8, 10)

    def getImage(self, x, y):
        """ It returns the appropriate sprite for Pac-Man. The sprite sheet has 32*32-pixel tiles (BASETILEWIDTH*BASETILEHEIGHT), but PacMan is twice as big as a tile."""
        return Spritesheet.getImage(self, x, y, 2*TILEWIDTH, 2*TILEHEIGHT)

    def update(self, dt):
        """ It is used by the class Pacman to update its image: self.sprites.update(dt). We call the update method of our animation class to tell us which image we need to get from the sprite image.""" 	
        if self.entity.alive == True:
            if self.entity.direction == LEFT:
                self.entity.image = self.getImage(*self.animations[LEFT].update(dt))
                self.stopimage = (8, 8)
            elif self.entity.direction == RIGHT:
                self.entity.image = self.getImage(*self.animations[RIGHT].update(dt))
                self.stopimage = (10, 8)
            elif self.entity.direction == DOWN:
                self.entity.image = self.getImage(*self.animations[DOWN].update(dt))
                self.stopimage = (8, 10)
            elif self.entity.direction == UP:
                self.entity.image = self.getImage(*self.animations[UP].update(dt))
                self.stopimage = (10, 10)
            elif self.entity.direction == STOP:
                self.entity.image = self.getImage(*self.stopimage)
        else:
           self.entity.image = self.getImage(*self.animations[DEATH].update(dt))

    def reset(self):
        for key in list(self.animations.keys()):
            self.animations[key].reset()

If you think about it, Pac-Man, our four ghosts, and the game’s fruits can be rendered the same way. We create a new variable or attribute “image” in their parent class “Entity”. It will hold the image that we need to display from the sprite file that represents the entity in a particular moment (the update(self, dt) method is in charge of updating this image).

We will draw this image on the screen in the render() method.

class Entity(object):
    def __init__(self, node, maze = None):
        [...]
        self.image = None

    def render(self, screen):
        if self.visible:
            if self.image is not None:
                p = self.position - myVector(TILEWIDTH, TILEHEIGHT)/2
                screen.blit(self.image, p.getVector())
            else:
                p = self.position.getVectorAsInt()
                pygame.draw.circle(screen, self.color, p, self.radius)

class GhostSprites(Spritesheet):
     """ A class that contains the ghosts' sprites. It inherits from Spritesheet. 
    Our ghosts classes have an attribute that is an object of this class: self.sprites = GhostSprites(self)"""
    def __init__(self, entity):
        Spritesheet._init__(self)
        self.x = {BLINKY:0, PINKY:2, INKY:4, CLYDE:6}
        self.entity = entity
        self.entity.image = self.getStartImage()
        self.death = myAnimator(((12, 6), (14, 6), (16, 6), (18, 6)), loop=False, superSpeed=True)
               
    def getStartImage(self):
        """ It returns the image we want the ghost to begin the game with. It is located in column (0, 2, 4, 6) and row 0 on the sprite sheet.""" 
        return self.getImage(self.x[self.entity.name], 0)

    def getImage(self, x, y):
        """ It returns the appropriate sprites for our ghosts. The sprite sheet has 32*32-pixel tiles (BASETILEWIDTH*BASETILEHEIGHT), but a ghost is twice as big as a tile."""
        return Spritesheet.getImage(self, x, y, 2*TILEWIDTH, 2*TILEHEIGHT)

    def update(self, dt):
         """ It is used by the ghosts classes to update their images: self.sprites.update(dt). When the ghost is dead, we call the update method of our animation class to tell us which image we need to get from the sprite image.""" 
        x = self.x[self.entity.name]
        if self.entity.mode == DEATH:
            self.entity.image = self.getImage(*self.death.update(dt)) 
        elif self.entity.mode in [SCATTER, CHASE]:
            if self.entity.direction == LEFT:
                self.entity.image = self.getImage(x, 4)
            elif self.entity.direction == RIGHT:
                self.entity.image = self.getImage(x, 6)
            elif self.entity.direction == DOWN:
                self.entity.image = self.getImage(x, 2)
            elif self.entity.direction == UP:
                self.entity.image = self.getImage(x, 0)
        elif self.entity.mode == FREIGHT:
            self.entity.image = self.getImage(10, 0)

You should observe that our ghosts are drawn with a blue sprite (10, 0) when they are in FREIGHT mode and only with their eyes (column 8, row: 4, 6, 2, and 8) when in ZOMBIE mode.

        elif self.entity.mode == ZOMBIE:
            if self.entity.direction == LEFT:
                self.entity.image = self.getImage(8, 4)
            elif self.entity.direction == RIGHT:
                self.entity.image = self.getImage(8, 6)
            elif self.entity.direction == DOWN:
                self.entity.image = self.getImage(8, 2)
            elif self.entity.direction == UP:
                self.entity.image = self.getImage(8, 0)
            
class FruitSprites(Spritesheet):
     """ A class that contains the fruits' sprites. It inherits from Spritesheet. 
    Our fruit class has an attribute that is an object of this class: self.sprites = FruitSprites(self, level). """
    def __init__(self, entity, level):
        Spritesheet._init__(self)
        self.entity = entity
        self.entity.image = self.getStartImage()

    def getStartImage(self, level):
        # Depending on the level, we get a different fruit.
        level = level % 6
        if level <= 2:
            x = 16 + (level * 2) 
            y = 8 
        else:
            level = level - 3
            x = 16 + (level * 2) 
            y = 10 

        return self.getImage(x, y)

    def getImage(self, x, y):
        return Spritesheet.getImage(self, x, y, 2*TILEWIDTH, 2*TILEHEIGHT)

class LifeSprites(Spritesheet):
    """ A class that contains the lives' sprites. It inherits from Spritesheet. 
    Our game class has an attribute that is an object of this class: self.lifesprites = LifeSprites(self.lives). """
    def __init__(self, numlives):
        Spritesheet._init__(self)
        self.resetLives(numlives)

    """ When Pac-Man dies, the game object calls this method (self.lifesprites.removeImage()) to remove one of Pac-Man's lives (more technically one of its images)."""
    def removeImage(self):
        if len(self.images) > 0:
            self.images.pop(0)
    
    # When we reset the game, we reset the number of Pac-Man's lives. Every life is represented by Pac-Man heading left with his mouth slightly opened (0, 10). self.images is a list with as many images as Pac-Man's lives.
    def resetLives(self, numlives):
        self.images = []
        for i in range(numlives):
            self.images.append(self.getImage(0, 10))

    def getImage(self, x, y):
        return Spritesheet.getImage(self, x, y, 2*TILEWIDTH, 2*TILEHEIGHT)

class MazeSprites(Spritesheet):

This is a class that contains the maze’ sprites that we need in our game. It inherits from Spritesheet. Our game class has an attribute that is an object of this class: self.mazesprites = MazeSprites(MAZEFILE, MAZEROTATION). Observe that the 18th row is different: G - - - - - + - - n 5 X X X X X X 5 n - - + - - - - - G

    def __init__(self, spritefile, rotfile):
        Spritesheet._init__(self)
        self.data = self.readMazeFile(spritefile)
        self.rotdata = self.readMazeFile(rotfile)

    def getImage(self, x, y):
        return Spritesheet.getImage(self, x, y, TILEWIDTH, TILEHEIGHT)

    def readMazeFile(self, spritefile):
        with open(spritefile) as f:
            lines = f.readlines()

        lines = [line.replace(' ', '') for line in lines]
        return lines

Immediately after our game object creates an instance of the class MazeSprites (self.mazesprites = MazeSprites(MAZEFILE, MAZEROTATION)), it calls this method to draw the maze on the background (self.background = self.mazesprites.constructBackground(self.background, self.level%5)).

The first line in the game’s render method will blit this surface on the screen: self.screen.blit(self.background, (0, 0)).

    def constructBackground(self, background, y):
        for row, line in enumerate(self.data):
            for col, character in enumerate(line):
                # There are 10 sprites that define our maze's layout. Each one of them can be rotated 90, 180, and 270°. spritefile (MAZEFILE, self.data) is the maze's file where each digit or number (0-9) represent a maze's sprite and rotfile (MAZEROTATION, self.rotdata) is a second file where we define the rotations for these sprites. 
                if self.data[row][col].isdigit():
                    x = int(self.data[row][col]) + 12 # Observe that the maze's sprites are located on (x+12, y). The "y" allows the maze to look "a bit" different depending on the level. 
                    sprite = self.getImage(x, y)
                    rotval = int(self.rotdata[row][col])
                    sprite = self.rotate(sprite, rotval) # The sprite is rotated 0*90°, 1*90°, 2*90° or 3*90° i.e., 0°, 90°, 180°, or 270°.
                    background.blit(sprite, (col*TILEWIDTH, row*TILEHEIGHT))
                elif self.data[row][col] == '=': # This is the door to the ghosts' home.
                    sprite = self.getImage(10, 4)
                    background.blit(sprite, (col*TILEWIDTH, row*TILEHEIGHT))

        return background

    def rotate(self, sprite, value):
       # It rotates the "sprite" image "value*90" degrees counterclockwise.
       return pygame.transform.rotate(sprite, value*90)

my Animator class

The Animator class’ main attribute is “frames”. It is a list of (x, y) locations on the sprite sheet.

from settings import *

class myAnimator(object):
    def __init__(self, frames=[], loop=True, superSpeed=False):
        self.frames = frames
        self.current_frame = 0 # The current frame is zero, the first element in the frames list.
        self.loop = loop # The animation can loop, otherwise it will only play once.
        self.dt = 0
        self.finished = False # Initially, the animation is not finished.
        self.nframes = len(self.frames) # It defines the number of frames.
        self.speed = SPEEDANIMATION if not superSpeed else SUPERSPEEDANIMATION # This is the speed of the animation.

    def reset(self):
        """ It resets the animation."""
        self.current_frame = 0
        self.finished = False

    def update(self, dt):
        """ The update method iterates through the frames list and returns the current frame's (x, y) location."""
        if not self.finished:
            self.nextFrame(dt)
        if self.current_frame == self.nframes -1:
            if not self.loop:
                self.finished = True
                
        return self.frames[self.current_frame]

    def nextFrame(self, dt):
        """ It updates or increments the current_frame at the chosen speed."""
        self.dt += dt
        if self.dt >= self.speed:
            self.current_frame = (self.current_frame +1) % self.nframes
            self.dt = 0
Bitcoin donation

JustToThePoint Copyright © 2011 - 2024 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.