Pages

Breakout - Step 5 - Part 2




Goal:

Now we are going to finish step 5 by adding a high score table. This requires us to hack up a lot of our existing code so you may want to use a diff program to look at the changes between step 5 and step 4 so you can tell what is actually going on.

NameSprite

First of all we will create another text sprite just like the Score object. We are going to have two functions that make this look like ‘entering text’ - addLetter and removeLetter. All they do is change the string we are using and re-render the image so it can be displayed. We don’t need anything fancy, so this super simple implementation will work nicely.

class NameSprite(pygame.sprite.Sprite):
    """A sprite for the play to enter their name"""

    def __init__(self, xy):
        pygame.sprite.Sprite.__init__(self)
        self.xy = xy
        self.text = ''
        self.color = (255, 0, 0)
        self.font = pygame.font.Font(None, 35)  # load the default font, size 35
        self.reRender() # generate the image

    def addLetter(self, letter):
        """Adds the given letter"""
        self.text += str(letter)
        self.reRender()

    def removeLetter(self):
        if len(self.text) == 1:
            self.text = ''
        else:
            self.text = self.text[:-1]
        self.reRender()

    def reRender(self):
        """Updates the text."""
        self.image = self.font.render(self.text, True, self.color)
        self.rect = self.image.get_rect()
        self.rect.center = self.xy

Game.init

When the game is over we want the player to enter their name if they set a high score, so we are going to add another ‘state’ of the game. We also need another render group for the name sprite and instruction text.

    self.enteringname = False

    # sprite group for name block
    self.namesprites = pygame.sprite.RenderUpdates()

Game.run

Now we are going to add another if block to our run method so we can render the name sprite if necessary. This isn’t the cleanest way to do things; normally I would set up a game this complicated with a more thorough game state system, but we didn’t need it until now so we are just going to make it work.

  # entering name, render name sprite
  elif self.enteringname:
      font = pygame.font.Font(None, 35)  # load the default font, size 50
      color = (255, 50, 0)
      nameimage = font.render('Enter Name:', True, color)
      namerect = nameimage.get_rect()
      namerect.center = 260, 250
      self.window.blit(nameimage,namerect)

      self.namesprites.clear(self.window, self.background)
      dirty = self.namesprites.draw(self.window)
      pygame.display.update(dirty+[namerect])

Game.handleInput

Now we are going to hack up our input method. Again, this would be much cleaner with a game state system. We are going to change the key down block to send letters to the name sprite if we are entering name or do our normal paddle movement if not. All I did to make this work was create a big string with all the letters I wanted to allow then check the ascii equivalent for each key pressed. If it wasn’t on my list or threw an exception (not a renderable key), I just ignore it. Finally, I tweak the keyup if statement so it doesn’t do anything when we are entering the name.

      # if entering name, save keys
      if self.enteringname:

          # if backspace, remove last letter until empty string
          if event.key == K_BACKSPACE:
              self.namesprite.removeLetter()

          # if enter, self.nameEntered with name
          elif event.key == K_RETURN:
              self.nameEntered()

          else:
              try:
                  char = chr(event.key)
                  # all the characters we want to allow
                  if str(char) in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_':
                      self.namesprite.addLetter(char)
              except:
                  pass    # exception from not being able to get char, dont care

      # not entering name
      else:
          # paddle control
          if event.key == K_a or event.key == K_LEFT:
              self.paddle.left()
          if event.key == K_d or event.key == K_RIGHT:
              self.paddle.right()

          # serve with space if the ball isn't moving
          if event.key == K_SPACE:
              if self.isReset:
                  self.ball.serve()
                  self.isReset = False

  elif event.type == KEYUP and not self.enteringname:

Game.newLevel

We are going to tweak our newLevel code. I’m going to move the message a bit and we need to display our highscores.

  endmessagerect.center = (260, 135)

  # handle showing and inputing high scores
  self.handleHighScores()

Game.reset

Same as above - I’m moving the you lose message and calling our method to handle the high scores. Before we had a ‘Press Esc to Quit’ message, which I removed since we are going to display the high score table now.

  endmessagerect.center = (260, 135)

  # show high score table
  self.handleHighScores()

Game.handleHighScores

This is the function we call at the end of the game, whether the player won or lost. It gets the current high score table and checks if the user needs to be on it. If so, we start the name entering process by creating the name sprite and setting the enteringname variable to enable all the changes we made above. If not, it just renders the high score table.

  def handleHighScores(self):
      """Called to prompt the user to enter a new
      high score name and show the high score table."""

      # load high scores
      highscores = self.parseHighScores()

      # if current score belongs on the table
      if self.score.score > int(highscores[-1][1]):
          # prompt for user name
          self.enteringname = True
          self.namesprite = NameSprite( (260, 310) )
          self.namesprites.add(self.namesprite)

      # otherwise just show table
      else:
          self.showHighScores(highscores)

Game.nameEntered

This is called when the player presses enter while putting in their name. We draw a blank background to overwrite everything, put the player’s score into the high score table, save the table, and render the high score list. Notice how simple our high scores are – it is literally a text file of lines with a name and their score separated by a colon. This is super insecure but this is all local so we don’t really care if the player wants to ‘hack’ it and change their score to 999999999999 with a simple text editor.

  def nameEntered(self):
      self.enteringname = False
      username = self.namesprite.text

      # blit the background onto the window
      self.window.blit(self.background, (0,0))
      # flip the display so the background is on there
      pygame.display.flip()

      # load high scores
      highscores = self.parseHighScores()

      # insert the player's score into the highscore table
      newscores = []
      for name, score in highscores:
          if self.score.score > int(score):
              newscores.append((username, str(self.score.score)))
              self.score.score = 0    # set to 0 so we dont add it again
          newscores.append((name, score))
      newscores = newscores[0:10] # only take 10

      # write scores to high score table
      highscorefile = 'highscores.txt'
      f = open(highscorefile, 'w')
      for name, score in newscores:
          f.write("%s:%s\n" % (name, score))
      f.close()

      # display high scores
      self.showHighScores(newscores)

Game.parseHighScores

We have been calling this every time we need the list of high scores - now you get to see how it works. First of all we check if the high score file exists – if not we don’t want to throw an exception and crash the game, we just want to create a default list. If it does exist, we read the file and parse it. Either way, we return a list of the high scores with each entry being another list of the form [name, score].

    def parseHighScores(self):
        """Parses the high score table and returns a
        list of scores and their owners"""
        highscorefile = 'highscores.txt'
        if os.path.isfile(highscorefile):
            # read the file into lines
            f = open(highscorefile, 'r')
            lines = f.readlines()
            # break lines into length 2 lists [name, score]
            scores = []
            for line in lines:
                scores.append( line.strip().split(':'))
            return scores
        else:
            # generate default highscore table
            f = open(highscorefile, 'w')
            f.write("""JJJ:10000
III:9000
HHH:8000
GGG:7000
FFF:6000
EEE:5000
DDD:4000
CCC:3000
BBB:2000
AAA:1000""")
            f.close()
            # call method again - will load the scores we just wrote this time
            return self.parseHighScores()

Game.showHighScores

This is our very last function. We take the list of scores created in the function above and render it on screen by looping through the scores and blitting text images on the window. To give it a slightly retro look I lined up the names on the left and the scores on the right, then drew dots to connect them.

def showHighScores(self, scores):
    """Draws the high score table onto the screen."""
    font = pygame.font.Font(None, 35)  # load the default font, size 50
    color = (255, 50, 0)

    for i in range(len(scores)):
        name, score = scores[i]
        # render name
        nameimage = font.render(name, True, color)
        namerect = nameimage.get_rect()
        namerect.left, namerect.y = 40, 100 + (i*(namerect.height + 20))
        self.window.blit(nameimage,namerect)

        # render score
        scoreimage = font.render(score, True, color)
        scorerect = scoreimage.get_rect()
        scorerect.right, scorerect.y = 480, namerect.y
        self.window.blit(scoreimage, scorerect)

        # draw dots from name until score
        for d in range(namerect.right + 25, scorerect.left-10, 25):
            pygame.draw.rect(self.window, color, pygame.Rect(d, scorerect.centery, 5, 5))

    # flip display
    pygame.display.flip()