Keeping it Small and Simple

2008.03.01

Pygame tutorial #9: first improvements to the game

Filed under: Pygame Tutorial — Tags: , , , , — Lorenzo E. Danielsson @ 21:35

In the last pygame tutorial we wrote a functional, but not very interesting worm game. In this tutorial and the next I will making various small improvements to the game. After that, I will finally leave the worm game and move on to other things. There will still be improvements to make, but those will be left as an exercise for you.

Bigger food

One problem with the game that we developed last time was that the game is insanely difficult unless your eyesight happens to be really good (which mine is not). Let’s start by sizing up the food a little. I will use a method called pygame.draw.rect which (surprise, suprise) draws a rectangle (and squares if width and height are equal). We modify the draw method of the Food class:

1 def draw(self):
2     pygame.draw.rect(self.surface, self.color, (self.x, self.y, 3, 3), 0)
3
4

This draws a square at the point (x, y). The width and the height are both 3 pixels. The final zero is the line width. A line width of zero draws a filled rectangle, which is what we want here.

To check if the worm has eaten the food we add a new method to Food, called check:


1 def check(self, x, y):
2     if x < self.x or x > self.x + 3:
3         return False
4     elif y < self.y or y > self.y + 3:
5         return False
6     else:
7         return True
8

This will check if the point (x, y) is a point within the food square. The method will return True if this is the case, or False if it isn’t.

Finally, in the game loop we need to change the test for if the worm has eaten the food. Instead of using the location() calls, we utilize the new check() method:

1 elif food.check(worm.x, worm.y):
2     score += 1
3     worm.eat()
4     print "Score: %d" % score
5     food = Food(screen)
6

Improving the keyboard handler

Another thing you will have noticed while playing is that it is far too easy to crash into yourself by “reversing” you direction. For example, if the worm is moving upwards, pressing down will cause the worm to crash onto itself. Let’s prevent that from happening:


 1 def event(self, event):
 2     """ Handle keyboard events. """
 3     if event.key == pygame.K_UP:
 4         if self.vy == 1: return
 5         self.vx = 0
 6         self.vy = -1
 7     elif event.key == pygame.K_DOWN:
 8         if self.vy == -1: return
 9         self.vx = 0
10         self.vy = 1
11     elif event.key == pygame.K_LEFT:
12         if self.vx == 1: return
13         self.vx = -1
14         self.vy = 0
15     elif event.key == pygame.K_RIGHT:
16         if self.vx == -1: return
17         self.vx = 1
18         self.vy = 0
19

For each key, there is one extra test here. This should not be difficult to understand.

A little optimization

Our little game could do with some optimization. The call to screen.fill() each time we run the loop slows the game down quite a bit. Also, since we are redrawing entire worm all the game should get just a little bit slower each time the worm’s length increases.

But why do we draw the entire worm? If we skip clearing the screen all the time, there are only to points we should draw: the first one and the last one. The last point of the worm should be plotted with the color of the background. We can change the draw() method in the worm class like this:


1 def draw(self):
2     #for x, y in self.body:
3     #    self.surface.set_at((x, y), self.color)
4     x, y = self.body[0]
5     self.surface.set_at((x, y), self.color)
6     x, y = self.body[-1]
7     self.surface.set_at((x, y), (0, 0, 0))
8
9

This requires us to comment out the screen.fill() call in the game loop. But we also need to be able to erase the food once it has been eaten. We add an erase() method to Food. This looks similar to the draw() method, but uses a different color:


1 def erase(self):
2     pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, 3, 3), 0)
3
4

Of course, we have to remember to call the erase() method each time the worm eats the food.

Adding sound

Let’s make the worm make a chomping sound any time it eats the food. To do this we first have to initialize the mixer and load a sample. You can search on the ‘Net for a sound file, or if you have a microphone, record your own, using software like sox. I recorded myself saying “GULP!” and saved the file as chomp.wav in the same directory as the worm game. It’s a good idea to make sure that the sound is short.


1 pygame.mixer.init()
2 chomp = pygame.mixer.Sound("chomp.wav")

Once we have done this, the variable chomp will hold a Sound object that we can play when we want to. In our case that is any time the worm eats the food.

The improved game

Here is the full code for the game as it is now:

  1 #! /usr/bin/env python
  2
  3 # A simple worm game, 2nd attempt.
  4
  5 import pygame
  6 import random
  7
  8 class Worm:
  9     def __init__(self, surface):
 10         self.surface = surface
 11         self.x = surface.get_width() / 2
 12         self.y = surface.get_height() / 2
 13         self.length = 1
 14         self.grow_to = 50
 15         self.vx = 0
 16         self.vy = -1
 17         self.body = []
 18         self.crashed = False
 19         self.color = 255, 255, 0
 20
 21     def eat(self):
 22         self.grow_to += 25
 23
 24     def event(self, event):
 25         """ Handle keyboard events. """
 26         if event.key == pygame.K_UP:
 27             if self.vy == 1: return
 28             self.vx = 0
 29             self.vy = -1
 30         elif event.key == pygame.K_DOWN:
 31             if self.vy == -1: return
 32             self.vx = 0
 33             self.vy = 1
 34         elif event.key == pygame.K_LEFT:
 35             if self.vx == 1: return
 36             self.vx = -1
 37             self.vy = 0
 38         elif event.key == pygame.K_RIGHT:
 39             if self.vx == -1: return
 40             self.vx = 1
 41             self.vy = 0
 42
 43     def move(self):
 44         """ Move the worm. """
 45         self.x += self.vx
 46         self.y += self.vy
 47
 48         if (self.x, self.y) in self.body:
 49             self.crashed = True
 50
 51         self.body.insert(0, (self.x, self.y))
 52
 53         if (self.grow_to > self.length):
 54             self.length += 1
 55
 56         if len(self.body) > self.length:
 57             self.body.pop()
 58
 59     def draw(self):
 60         #for x, y in self.body:
 61         #    self.surface.set_at((x, y), self.color)
 62         x, y = self.body[0]
 63         self.surface.set_at((x, y), self.color)
 64         x, y = self.body[-1]
 65         self.surface.set_at((x, y), (0, 0, 0))
 66
 67
 68 class Food:
 69     def __init__(self, surface):
 70         self.surface = surface
 71         self.x = random.randint(0, surface.get_width())
 72         self.y = random.randint(0, surface.get_height())
 73         self.color = 255, 255, 255
 74
 75     def draw(self):
 76         pygame.draw.rect(self.surface, self.color, (self.x, self.y, 3, 3), 0)
 77
 78     def erase(self):
 79         pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, 3, 3), 0)
 80
 81     def check(self, x, y):
 82         if x < self.x or x > self.x + 3:
 83             return False
 84         elif y < self.y or y > self.y + 3:
 85             return False
 86         else:
 87             return True
 88
 89 w = 500
 90 h = 500
 91
 92 screen = pygame.display.set_mode((w, h))
 93 clock = pygame.time.Clock()
 94
 95 pygame.mixer.init()
 96 chomp = pygame.mixer.Sound("chomp.wav")
 97
 98 score = 0
 99 worm = Worm(screen)
100 food = Food(screen)
101 running = True
102
103 while running:
104     #screen.fill((0, 0, 0))
105     worm.move()
106     worm.draw()
107     food.draw()
108
109     if worm.crashed:
110         running = False
111     elif worm.x <= 0 or worm.x >= w – 1:
112         running = False
113     elif worm.y <= 0 or worm.y >= h – 1:
114         running = False
115     elif food.check(worm.x, worm.y):
116         score += 1
117         worm.eat()
118         chomp.play()
119         print "Score: %d" % score
120         food.erase()
121         food = Food(screen)
122     
123     for event in pygame.event.get():
124         if event.type == pygame.QUIT:
125             running = False
126         elif event.type == pygame.KEYDOWN:
127             worm.event(event)
128
129     pygame.display.flip()
130     clock.tick(240)

Exercises

1. If you play the game for a while you will notice that from time to time the food gets placed in a location where you cannot reach it. This happens when the food is placed at the right side or the bottom of the game screen. Why is that? What modification do you have to make to the Food constructor to prevent this from happening?

2. Modify the keyboard handler to only use the left and right keys. Pressing right should change the mouse direction clock-wise. Left will change the direction counter-clockwise.

3. Give the game a different background than black.

4. Notice how Food.erase() can result in the worm being “cut”. Change the game so that any time the food is eaten, after the call to Food.erase() the entire worm gets redrawn. You may want to have add an optional flag argument to Worm.draw(). If the flag is true, the entire worm is redrawn. Otherwise only the first and last pixels are drawn.

5. Add a sound when the worm crashes.

6. We increased the size of the food by drawing rectangles instead of pixels. Could the same be applied to the worm? Experiment with this and see what the issues are, if any.

5 Comments »

  1. […] Pygame tutorial #9: first improvements to the game In the last pygame tutorial we wrote a functional, but not very interesting worm game. In this tutorial and the next I […] […]

    Pingback by Top Posts « WordPress.com — 2008.03.03 @ 00:00

  2. I came up with my own code that’s a little bit different from yours, see if you like it. It’s full screen and does everything in the main screen:

    import pygame
    from pygame.color import THECOLORS
    import random
    pygame.init()

    class worm:
    def __init__(self,surface,x,y,length):
    self.surface = surface
    self.x = x
    self.y = y
    self.length = length
    self.dir_x = 0
    self.dir_y = -1
    self.body = []
    self.crashed = False

    def key_event(self,event):
    if event.key == pygame.K_UP and self.dir_y != 1:
    self.dir_x = 0
    self.dir_y = -1
    elif event.key == pygame.K_DOWN and self.dir_y != -1:
    self.dir_x = 0
    self.dir_y = 1
    elif event.key == pygame.K_LEFT and self.dir_x != 1:
    self.dir_x = -1
    self.dir_y = 0
    elif event.key == pygame.K_RIGHT and self.dir_x != -1:
    self.dir_x = 1
    self.dir_y = 0
    elif event.key == pygame.K_RETURN:
    pause()

    def move(self):
    self.x += self.dir_x
    self.y += self.dir_y

    r, g, b, a = self.surface.get_at((self.x,self.y))
    if (r, g, b) == (255, 255, 255):
    self.crashed = True

    self.body.insert(0, (self.x, self.y))

    if len(self.body) > self.length:
    self.body.pop()

    def draw(self):
    x, y = self.body[0]
    self.surface.set_at((x,y),(255,255,255))
    x, y = self.body[-1]
    self.surface.set_at((x,y),(0,0,0))

    class redfood:
    def __init__(self,screen,width,height):
    self.screen = screen
    self.x = random.randint(5,width-5)
    self.y = random.randint(5,height-5)
    self.eat = 0
    self.points = 15

    def test(self,wx,wy):
    if self.x = wx and self.y = wy:
    self.eat = 1

    def draw(self):
    pygame.draw.rect(self.screen,(255,0,0),(self.x,self.y,3,3),0)

    def erase(self):
    pygame.draw.rect(self.screen,(0,0,0),(self.x,self.y,3,3),0)

    class greenfood:
    def __init__(self,screen,width,height):
    self.screen = screen
    self.x = random.randint(5,width-5)
    self.y = random.randint(5,height-5)
    self.eat = 0
    self.points = 10

    def test(self,wx,wy):
    if self.x = wx and self.y = wy:
    self.eat = 1

    def draw(self):
    pygame.draw.rect(self.screen,(0,255,0),(self.x,self.y,3,3),0)

    def erase(self):
    pygame.draw.rect(self.screen,(0,0,0),(self.x,self.y,3,3),0)

    class bluefood:
    def __init__(self,screen,width,height):
    self.screen = screen
    self.x = random.randint(5,width-5)
    self.y = random.randint(5,height-5)
    self.eat = 0
    self.points = 5

    def test(self,wx,wy):
    if self.x = wx and self.y = wy:
    self.eat = 1

    def draw(self):
    pygame.draw.rect(self.screen,(0,0,255),(self.x,self.y,3,3),0)

    def erase(self):
    pygame.draw.rect(self.screen,(0,0,0),(self.x,self.y,3,3),0)

    def pause():
    loop = 1
    while loop:
    for e in pygame.event.get():
    if e.type == pygame.KEYDOWN and e.key == pygame.K_RETURN:
    loop = 0

    def score(score,height,screen):
    font = pygame.font.Font(None, 20)
    text = font.render(‘%i’%points,0,(0,0,255))
    textrect = text.get_rect(centery=height-10)
    eraserect = textrect
    eraserect[2] += 10
    pygame.draw.rect(screen,(0,0,0),eraserect)
    screen.blit(text,textrect)

    pygame.init()
    width = 640
    height = 400
    clock = pygame.time.Clock()
    screen = pygame.display.set_mode((width,height),pygame.FULLSCREEN)
    pygame.mouse.set_visible(0)
    screen.fill((255,255,255))
    font = pygame.font.Font(None,25)
    text1 = font.render(‘Use the arrows to control the direction of your snake.’,1,(0,255,0))
    textrect1 = text1.get_rect(centerx=width/2,centery=height/5)
    screen.blit(text1,textrect1)
    text1 = font.render(‘Collect food to gain points and grow.’,1,(0,255,0))
    textrect1 = text1.get_rect(centerx=width/2,centery=2*height/5)
    screen.blit(text1,textrect1)
    text1 = font.render(‘blue = 5pts, green = 10pts, red = 15pts.’,1,(0,255,0))
    textrect1 = text1.get_rect(centerx=width/2,centery=3*height/5)
    screen.blit(text1,textrect1)
    text1 = font.render(‘Press [enter] in game to pause’,1,(0,255,0))
    textrect1 = text1.get_rect(centerx=width/2,centery=4*height/5)
    screen.blit(text1,textrect1)
    font2 = pygame.font.Font(None, 15)
    text1 = font.render(‘Press [enter] to continue.’,1,(0,0,0))
    textrect1 = text1.get_rect(centerx=width/2,centery=height-30)
    screen.blit(text1,textrect1)
    pygame.display.flip()
    waiting = 1
    while waiting:
    for event in pygame.event.get():
    if event.type == pygame.QUIT:
    waiting = 0
    pygame.quit()
    elif event.type == pygame.KEYDOWN:
    if event.key == pygame.K_RETURN:
    waiting = 0

    done = 0
    while not done:
    pygame.init()
    screen = pygame.display.set_mode((width,height),pygame.FULLSCREEN)
    running = True
    w = worm(screen, width/2, height/2, 200)
    f = bluefood(screen, width, height)
    points = 0

    while running:
    w.move()
    w.draw()
    f.draw()
    f.test(w.x,w.y)
    score(points,height,screen)

    if w.crashed or w.x = width-1 or w.y = height-1:
    print ‘Crash!’
    running = False

    if f.eat:
    w.length += 200
    points += f.points
    f.erase()
    r = random.randint(1,6)
    if r == 1:
    f = redfood(screen,width,height)
    elif r <= 3:
    f = greenfood(screen,width,height)
    else:
    f = bluefood(screen,width,height)
    for i in range(1,4):
    x,y = w.body[i]
    screen.set_at((x,y),(255,255,255))

    for event in pygame.event.get():
    if event.type == pygame.QUIT:
    running = False
    elif event.type == pygame.KEYDOWN:
    w.key_event(event)

    pygame.display.flip()
    clock.tick(200)

    font = pygame.font.Font(None,48)
    text1 = font.render(‘You scored %i points!’%points,1,(0,255,0))
    textrect1 = text1.get_rect(centerx=width/2,centery=height/3)
    screen.blit(text1,textrect1)
    text = font.render(‘Press [enter] to play again.’,1,(255,0,0))
    textrect = text.get_rect(centerx=width/2,centery=height/2)
    screen.blit(text, textrect)
    text2 = font.render(‘Press [esc] to quit.’,1,(0,0,255))
    textrect2 = text2.get_rect(centerx=width/2,centery=2*height/3)
    screen.blit(text2,textrect2)
    pygame.display.flip()
    waiting = 1
    while waiting:
    for event in pygame.event.get():
    if event.type == pygame.QUIT:
    waiting = 0
    again = 0
    elif event.type == pygame.KEYDOWN:
    if event.key == pygame.K_RETURN:
    again = 1
    waiting = 0
    elif event.key == pygame.K_ESCAPE:
    again = 0
    waiting = 0

    if not again:
    break

    pygame.quit()

    it’s a bit longer, but more advanced…and i like the different kind of food feature and the score is always useful

    Comment by Bill — 2008.03.13 @ 03:21

  3. i don’t know why that last comment didn’t indent…o well…you can probably figure it out

    Comment by Bill — 2008.03.13 @ 03:22

  4. @Bill: good stuff. Don’t worry about the indentation, as you said, it’s simple enough to figure out.

    My code is intentionally lame. I leave things out and I am more concerned about readable code than anything else. Very readable Python is not always good Python..

    The idea is that people should improve what I did, that is my code should be a starting point for their own experimentation. That is what they will really learn from.

    You have done exactly that, so good work!

    I’m working on tutorial #10 which will contain some of what you added here, such as the score. I’ve just been dragged down by work lately. 😦

    Comment by Lorenzo E. Danielsson — 2008.03.13 @ 09:03

  5. Hmm. You should make few tutorials about using sprites.. it would be useful. 😉 But thanks anyway for great tut’s!

    Comment by Bampi — 2008.07.30 @ 22:23


RSS feed for comments on this post. TrackBack URI

Leave a comment

Create a free website or blog at WordPress.com.