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&quot ;)

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&quot ;)
 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.

2008.02.27

Pygame tutorial #8: the worm game

Filed under: Pygame Tutorial — Tags: , , , — Lorenzo E. Danielsson @ 11:46

After the little detour I took with tutorial #7, we are going back to writing our little worm game. I am going to write a first, (barely) playable version here, and then we are gradually going to improve upon it.

As always, I am taking things really slow. That way I hope everybody can keep up. There are loads of other, more advanced pygame tutorials floating around for those who feel I’m moving too slowly. Many of them are really good.

Rules of the game

The idea behind the game is very simple. You control a worm that move around the screen looking for food. When the worm gets the food, you get some points and the worms grows longer. Crashing onto yourself or the borders ends the game.

I first played the worm game on a VIC-20, which means this was one of the first games I ever played.

The Worm class

We will expand a little upon the worm class the we developed earlier. I’m going to take it method by method. You will find the whole program at the end of the tutorial.

Let’s look at the constructor first. Here we just set up some information.


 1 def __init__(self, surface):
 2     self.surface = surface
 3     self.x = surface.get_width() / 2
 4     self.y = surface.get_height() / 2
 5     self.length = 1
 6     self.grow_to = 50
 7     self.vx = 0
 8     self.vy = -1
 9     self.body = []
10     self.crashed = False
11     self.color = 255, 255, 0

This constructor only takes the surface as an argument. We position to worm in the center of the screen initially (remember the x and y attributes hold the position of the worm’s head). The worm’s initial length is 1. I have added a new attribute, grow_to. As long as length is less than grow_to our worm hasn’t reached full length yet. This will allow the worm to grow, something that is important in the game.

Apart from that, most things should be straight-forward. The vx (horizontal velocity) and vy attributes control the direction of the worm. Each one of these can be -1, 0 or 1. Depending on the direction of motion. The color attribute should be obvious. The body attribute is an array holding all the positions of the worm’s body. Crashed is a flag that gets set to true if the worm bumps into itself.

The worm’s event() method is hopefully clear now. It looks like this:

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

The move() method has changed slightly. Let us look at what it does.


 1 def move(self):
 2     """ Move the worm. """
 3     self.x += self.vx
 4     self.y += self.vy
 5
 6     if (self.x, self.y) in self.body:
 7         self.crashed = True
 8
 9     self.body.insert(0, (self.x, self.y))
10
11     if (self.grow_to > self.length):
12         self.length += 1
13
14     if len(self.body) > self.length:
15         self.body.pop()
16

First we move the worm’s head to its new position. Where this is will depend on the values of vx and vy (note that exactly one of these will be non-zero). Next we check if the new position of the worm’s head exists in the worm’s body array. If it does, we have crashed onto ourselves and set the crashed flag.

The new position then gets inserted into the worm’s body array. We then check if the worm is growing. If it is, simply increase the worm’s length by 1. Finally, if we have reached full length, then we need to remove the last item of the body array. Make sure you understand how the different parts of this method work together to make the worm move and grow. See if you can figure out a few ways to improve it (it shouldn’t be too difficult once you’ve got the point of the code).

The draw() method should be familiar by now. It just plots out each one of the points in the worm’s body array.

1 def draw(self):
2     for x, y in self.body:
3         self.surface.set_at((x, y), self.color)
4
5

There is a new method that I’ve called position(). This will return the x and y-coordinate of the worm’s head. This is just there for convenience.


1 def position(self):
2     return self.x, self.y
3
4

Another new method is the eat() method. This just instructs the worm to start growing.


1 def eat(self):
2     self.grow_to += 25
3

That is how the Worm class works. That main thing that is new about it as compared to the one we did in tutorial #5 is that this worm has the ability to grow. In order to grow, this worm needs food, so let’s look at that next.

The Food class

The Food class needs a surface to draw itself onto, just like the Worm class. Apart from that it holds it x and y coordinates and color. It contains a method to draw itself and one to get its position in the form x, y. It looks as follows.


 1 class Food:
 2     def __init__(self, surface):
 3         self.surface = surface
 4         self.x = random.randint(0, surface.get_width())
 5         self.y = random.randint(0, surface.get_height())
 6         self.color = 255, 255, 255
 7
 8     def draw(self):
 9         self.surface.set_at((self.x, self.y), self.color)
10
11     def position(self):
12         return self.x, self.y
13

When a new Food is created, it places itself at a random location on the surface it gets passed.

The game

Putting our two classes to use, we come up with this:


  1 #! /usr/bin/env python
  2
  3 # A simple worm game.
  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             self.vx = 0
 28             self.vy = -1
 29         elif event.key == pygame.K_DOWN:
 30             self.vx = 0
 31             self.vy = 1
 32         elif event.key == pygame.K_LEFT:
 33             self.vx = -1
 34             self.vy = 0
 35         elif event.key == pygame.K_RIGHT:
 36             self.vx = 1
 37             self.vy = 0
 38
 39     def move(self):
 40         """ Move the worm. """
 41         self.x += self.vx
 42         self.y += self.vy
 43
 44         if (self.x, self.y) in self.body:
 45             self.crashed = True
 46
 47         self.body.insert(0, (self.x, self.y))
 48
 49         if (self.grow_to > self.length):
 50             self.length += 1
 51
 52         if len(self.body) > self.length:
 53             self.body.pop()
 54
 55     def draw(self):
 56         for x, y in self.body:
 57             self.surface.set_at((x, y), self.color)
 58
 59     def position(self):
 60         return self.x, self.y
 61
 62 class Food:
 63     def __init__(self, surface):
 64         self.surface = surface
 65         self.x = random.randint(0, surface.get_width())
 66         self.y = random.randint(0, surface.get_height())
 67         self.color = 255, 255, 255
 68
 69     def draw(self):
 70         self.surface.set_at((self.x, self.y), self.color)
 71
 72     def position(self):
 73         return self.x, self.y
 74
 75 w = 500
 76 h = 500
 77
 78 screen = pygame.display.set_mode((w, h))
 79 clock = pygame.time.Clock()
 80
 81 score = 0
 82 worm = Worm(screen)
 83 food = Food(screen)
 84 running = True
 85
 86 while running:
 87     screen.fill((0, 0, 0))
 88     worm.move()
 89     worm.draw()
 90     food.draw()
 91
 92     if worm.crashed:
 93         running = False
 94     elif worm.x <= 0 or worm.x >= w - 1:
 95         running = False
 96     elif worm.y <= 0 or worm.y >= h - 1:
 97         running = False
 98     elif worm.position() == food.position():
 99         score += 1
100         worm.eat()
101         print "Score: %d" % score
102         food = Food(screen)
103     
104     for event in pygame.event.get():
105         if event.type == pygame.QUIT:
106             running = False
107         elif event.type == pygame.KEYDOWN:
108             worm.event(event)
109
110     pygame.display.flip()
111     clock.tick(240)

We initialize a counter called score to 0 (unless you want to cheat of course). We create a our worm and an initial food item.

In the game loop we need to make sure we draw not only the worm but the food item as well. This is done with food.draw().

In addition to checking if the worm has crashed with itself or the borders, we check if the worm has eaten the food. This is where the position() methods in the Worm and Food classes comes in handy. If the worm’s head is at the same location as the food the worm has taken the food. We then increase the score and print the new score to the terminal (which sucks, I know, but don’t worry one of our improvements will be to get the score onto the game screen). We also need to signal the worm to start growing. We do that by calling worm.eat(). Finally, since the food item has been eaten, we need to generate a new one.

The rest of the program should be familiar by now.

Running it

The one thing you will notice quickly with this game is how annoyingly difficult it is to eat the food (unless you have really good eye-sight which I don’t). This will be the first improvement we have to make to the game.

Exercises

1. Ew! There are like magic numbers, EVERYWHERE! Find each one and change it. It’s often useful to create attributes. For instance the worm is hard-coded to grow 25 pixels longer each time it eats. You could change that to an attribute called grow_by (or whatever you like). That means it can be manipulated from outside the class.

2. Play around with the initial worm size and the amount that the worm grows by when it eats. By modifying these you can affect how difficult the game is when it first starts and how much more difficult it gets over time.

3. Currently the program ends as soon as the worm crashes. Change it so that the user gets asked if they want to play again.

4. Build upon exercise 2 to keep a track of the high score.

5. Imagine you wanted to turn this into a two-player game. What would you need to change in order to make the Worm class re-usable?

6. Add the concept of lives to the game. The player should start off with three lives. Each time they crash the lose a life. When they run out, the game is over.

7. Let the worm start at a random position.

8. Suppose you wanted to implement difficulty levels. What things would you vary in order to make the game more or less difficult?

Conclusion

Now that we have a functional game, we need to make it properly playable as well. The first thing we need to do is to make the food item a little larger so that our worm stands a reasonable chance of getting it. It would be nice if the worm could make a “chomp” sound when it takes a bite as well. That will be the topic for the next tutorial.

2008.02.19

Pygame tutorial #7: more on lines

Filed under: Pygame, Pygame Tutorial — Tags: , , — Lorenzo E. Danielsson @ 23:52

This tutorial is a bit of a special. I’ve heard from several people who have had difficulty with Drawing lines - Exercise 6 in tutorial #2. So I am going to go through how to solve it here.

Understand the problem

To understand the problem we are trying to solve we need to draw a bit so get out your pen and paper. If you have square ruled paper, it might make things a bit easier for you. Draw a horizontal and a vertical line as in the picture below.

net001.png

You can of course use more squares than I have, but this should be enough to demonstrate the principle. Next, draw a line from (0, -5) to (1, 0) as I have.

net002.png

In each image, I have used blue to indicate the newest line. It is not something you need to do in your drawing. Next line goes from (0, -4) to (2, 0).

net003.png

Next line: (0, -3) to (3, 0):

net004.png

Then (0, -2) to (4, 0):

net005.png

Draw a final line from (0, -1) to (5, 0):

net006.png

Now you have the principle behind the pattern in the exercise. To get the horizontal and vertical lines you could do (0, -6) to (0, 0) and (0, 0) to (6, 0). To sum it all up:

(0, -6) to (0, 0)
(0, -5) to (1, 0)
(0, -4) to (2, 0)
(0, -3) to (3, 0)
(0, -2) to (4, 0)
(0, -1) to (5, 0)
(0, 0) to (6, 0)

Translating to python

We obviously have two variables here. One is the y-coordinate of the first point in each line that varies from -6 to 0. The other one is the x-coordinate of the second point in each pair that goes from 0 to 6. The following Python snippet can achieve what we want.


1 for x, y in zip(range(-6, 0+1), range(0, 6+1)):
2     print "(0, %d) to (%d, 0)" % (x, y)
3

(Note that in python, range(a, b) will include all numbers from a to b-1. That is why I wrote 0+1 and 6+1. You could have written 1 and 7 respectively, but I just wanted to add clarity)

If we think about it, the two variables vary together. So we could select one of them, say x, and express y in terms of x. Since x goes from 0 to 6 at the same time as y goes from -6 to 0, we could say that for each pair of points:

y = x - 6

So we could, if we so choose, write the python code as follows instead:

1 for x in range(0, 6+1):
2     print "(0, %d) to (%d, 0)" % (x-6, x)
3

Got it so far? Good. :-)

Preparing to put this on the screen

First of all, we need to make a slight modification to what we have said so far. On the screen we need some space between each point. So instead of the x-coordinates being 0, 1, 2, 3, 4, 5 and 6, we would want them to be something like 0, 10, 20, 30, 40, 50 and 60. We will of course draw more lines as well.

But there is a more serious issue. I have been using Cartesian coordinates up to now. But screen coordinates are different. The top left hand corner of your screen is the point (0,0). As you move down the screen, y increases. So the lines we need to draw thus look as follows:

(0, 6) to (0, 0)
(0, 5) to (1, 0)
(0, 4) to (2, 0)
(0, 3) to (3, 0)
(0, 2) to (4, 0)
(0, 1) to (5, 0)
(0, 0) to (6, 0)

This means that the relationship between x and y is:

y = 6 - x

Make sure you understand this before you go on. Take as much time as you need to (hurry is a bad word in my vocabulary).

Now on to pygame

Time to use what we have just learned to put together a little program. The following program will solve the first part of the exercise.


 1 import pygame
 2
 3 screen = pygame.display.set_mode((500, 500))
 4 clock = pygame.time.Clock()
 5 running = True
 6 size = 250
 7 step = 10
 8
 9 for x in range(0, size+1, step):
10     pygame.draw.line(screen, (255, 255, 255), (0, 250-x), (x, 0))
11
12 pygame.display.flip()
13
14 while running:
15     for event in pygame.event.get():
16         if event.type == pygame.QUIT:
17             running = False
18
19     clock.tick()
20

Drawing in all corners

The second part of the exercise was to draw this pattern in all four corners of the screen. I suggest you go back to use the pen and paper and draw each figure so you get a good idea of how the coordinates vary in each case.

Next you need to remember to convert all the lines into screen coordinates. You will need to remember that “nets” on the right side need their x-coordinates to be in relation to the right-most screen coordinate, which is the width of the screen - 1 (if the width of the screen is 501 pixels, the coordinates go from 0 to 500).

The same applies to the “nets” at the bottom of the screen. The y-coordinates need to be expressed in relation to the height of the screen instead.

I am going to let you work out the math for yourself. It is not too difficult if you take your time and solve it one step at a time. Just follow the principle I have outlined already. I will give you the solution in code. If you are having difficulties, you can look at the code and “go backwards” to get to the math.


 1 import pygame
 2
 3 w = h = 500
 4 screen = pygame.display.set_mode((w+1, h+1))
 5 clock = pygame.time.Clock()
 6 running = True
 7 size = 250
 8 step = 10
 9 color = 255, 255, 255
10
11 for x in range(0, size+1, step):
12     pygame.draw.line(screen, color, (0, size-x), (x, 0))
13     pygame.draw.line(screen, color, (w - (size-x), 0), (w, x))
14     pygame.draw.line(screen, color, (w, h - (size-x)), (w-x, h))
15     pygame.draw.line(screen, color, (250-x, h), (0, h-x))
16
17 pygame.display.flip()
18
19 while running:
20     for event in pygame.event.get():
21         if event.type == pygame.QUIT:
22             running = False
23
24     clock.tick()
25

Note that the screen width is actually w+1 and the height is h+1. This is acceptable in my code because I make the rules. ;-) On a serious note, it makes the draw_line statements a little simpler.

You can (and should) experiment with size, step and color. Don’t worry if something ends up looking differently than you expected. If that happens, just take the time to understand why the program produces the results it does. This can be done with pen paper and a few debug statements (you could get the program to print out the coordinates of each line).

Animating it

Okay, let’s do something that is a little more interesting.


 1 #! /usr/bin/env python
 2
 3 import pygame
 4
 5 w = h = 500
 6 size = 250
 7 step = 10
 8 lines = []
 9 pos = 0
10 maxlines = 40
11
12 for x in range(0, size+1, step):
13     lines.append((0, size-x, x, 0))
14
15 for x in range(0, size+1, step):
16     lines.append((w - (size-x), 0, w, x))
17
18 for x in range(0, size+1, step):
19     lines.append((w, h - (size-x), w-x, h))
20
21 for x in range(0, size+1, step):
22     lines.append((size-x, h, 0, h-x))
23
24 screen = pygame.display.set_mode((w+1, h+1))
25 clock = pygame.time.Clock()
26 running = True
27
28 while running:
29     for event in pygame.event.get():
30         if event.type == pygame.QUIT:
31             running = False
32
33     screen.fill((0, 0, 0))
34     col = 0
35     cur = pos
36
37     for i in range(maxlines):
38         x1, y1, x2, y2 = lines[cur]
39         pygame.draw.line(screen, (col, col, col), (x1, y1), (x2, y2))
40
41         cur += 1
42         if cur >= len(lines): cur = 0
43         col += 240 / maxlines
44         
45     pos += 1
46     if pos >= len(lines): pos = 0
47
48     pygame.display.flip()
49     clock.tick(40)

I am not going to explain how it works here. Rather the following exercises are all related to this code. Solve the exercises as a guide to help you build up an understanding of how the program works.

Remember that as a programmer, one of the skills you will need to have is the ability to read code and make sense of it. That is the purpose of this. Just take it all one step at a time and you will be okay (I promise). If you are not sure of what a particular line does, comment it out or modify it in some way and see how it affects the result. That is an excellent and fun way to learn more.

Once you have been able to understand how the program works, see how you can improve upon it. There are several things you could do. You could look at optimizing it, making it more readable, cut out unnecessary variables, etc. Feel free to post your code.

Exercises

1. What date does the lines array hold?

2. What is the purpose of the pos variable?

3. What is the purpose of the maxlines variable? What happens when you change maxlines to 10? Or to 60? Try with different values for maxlines and see how it affects the program.

4. Why is it that I use four for loops to populate the lines array? What would happen if you used a single for loop containing all four appends? Is there any way you could re-write this part to use a single for loop without the lines ending up in the wrong order?

5. Why have I created the cur variable. It seems to always be initialized to the value of pos. Would it be possible to do away with cur and only use pos?

6. What on earth goes on in the loop that starts with for i in range(maxlines)?

7. What is it that causes the lines to “fade”?

8. Play around with the expression that changes the color value (col += 240 / maxlines)

9. Modify the program to draw red, green, blue or yellow lines. Better yet, make it use any color you prefer. Make sure that the lines still fade properly.

10. What is the purpose of this code?

pos += 1
if pos >= len(lines): pos = 0

11. Change the “tick” value. How does it affect the speed of the animation when you increase or decrease the value?

12. Can you reverse the direction of the animation?

Conclusion

Okay, that’s it for this time. Hope you enjoyed it. Don’t forget that curiosity is a good thing. Try things out. Modify the code. Any time you are uncertain about what something does, play around with it. You won’t damage your computer or your OS by messing around with the code.

Also, as the beginning of this tutorial was meant to show, there are times when pen and paper are a programmer’s best friend (sorry, Matz).

Most importantly, don’t give up! If I’m able to work things like this out, so can you.

2007.12.18

Pygame tutorial #6: from pixel to worm

Filed under: Pygame Tutorial, Python Programming — Tags: , , , — Lorenzo E. Danielsson @ 18:03

In the previous tutorial we learned how to plot individual pixels. We wrote a simple program where you move a pixel around the screen. Today we are going to turn our single pixel into a worm.

Two classic games that could easily be built from what we will look at today are “the worm game” and Tron. In this tutorial we will learn how to create the worm itself, and in the next tutorial we will turn it all into a little game that we can actually play.

A first try

Our worm will move across the screen. We can control it with the cursor keys. There are two things are worm must avoid at all costs: the screen borders and itself.

The following code implements just that.


 1 #! /usr/bin/env python
 2
 3 # Move a worm across the screen. Beware of borders and self!
 4
 5 import pygame
 6
 7 class Worm:
 8     """ A worm. """
 9
10     def __init__(self, surface, x, y, length):
11         self.surface = surface
12         self.x = x
13         self.y = y
14         self.length = length
15         self.dir_x = 0
16         self.dir_y = -1
17         self.body = []
18         self.crashed = False
19
20     def key_event(self, event):
21         """ Handle key events that affect the worm. """
22         if event.key == pygame.K_UP:
23             self.dir_x = 0
24             self.dir_y = -1
25         elif event.key == pygame.K_DOWN:
26             self.dir_x = 0
27             self.dir_y = 1
28         elif event.key == pygame.K_LEFT:
29             self.dir_x = -1
30             self.dir_y = 0
31         elif event.key == pygame.K_RIGHT:
32             self.dir_x = 1
33             self.dir_y = 0
34
35     def move(self):
36         """ Move the worm. """
37         self.x += self.dir_x
38         self.y += self.dir_y
39
40         if (self.x, self.y) in self.body:
41             self.crashed = True
42
43         self.body.insert(0, (self.x, self.y))
44
45         if len(self.body) > self.length:
46             self.body.pop()
47         
48     def draw(self):
49         for x, y in self.body:
50             self.surface.set_at((x, y), (255, 255, 255))
51
52 # Dimensions.
53 width = 640
54 height = 400
55
56 screen = pygame.display.set_mode((width, height))
57 clock = pygame.time.Clock()
58 running = True
59
60 # Our worm.
61 w = Worm(screen, width/2, height/2, 200)
62
63 while running:
64     screen.fill((0, 0, 0))
65     w.move()
66     w.draw()
67
68     if w.crashed or w.x <= 0 or w.x >= width-1 or w.y <= 0 or w.y >= height-1:
69         print "Crash!"
70         running = False
71
72     for event in pygame.event.get():
73         if event.type == pygame.QUIT:
74             running = False
75         elif event.type == pygame.KEYDOWN:
76             w.key_event(event)
77
78     pygame.display.flip()
79     clock.tick(240)

The first thing we do a define what a worm is. Notice how the functionality of the worm is contained within the class. The worm knows how to move itself, draw itself, and it handles the events that relate to it. (Note: if you have done Java programming before, you may wonder where your accessors and mutators are. Don’t worry about them. You don’t need them.)

The body of the worm is stored in a list. Each time we move one step, we insert the new position of the body into the beginning of the list and pop off the last list item. That keeps our worm constant in length. Actually, that is not entirely true. Our worm starts off being only a single pixel long. We have a bit of extra code that allows the worm to grow to its full length. A little bit of code analysis should make that clear.

The move() method also contains a check to see if the worm has crashed onto itself. What we do is see if the position of the worm’s head (self.x, self.y) happens to be at the same place as any part of the worm’s body, by comparing (self.x, self.y) with each position in the body list.

The draw() method simply plots all the points held in the body list. The main program should be fairly straight-forward by now. Notice that in addition to checking if the worm has crashed onto any of the borders, the worm object is also asked if it has crashed with itself.

Exercises

  1. Let the worm grow over time. You could for instance have a counter that decreases every frame, or every time the worm turns or whatever. When the counter reaches zero, you increase the length of the worm.
  2. Create a program that displays two worms. What changes do you need to make to the Worm class in order for this to work?
  3. Modify the worm to have a gradient effect. The head of the mouse should be white. Each body segment should gradually become darker and darker shades of gray.

Collision detection

In our first example, we checked if the worm had collided with itself by comparing the position of the worm’s head with all the positions of the body. That worked fine, but there are instances where it’s not practical to keep a lot of positions in a list. An example would be the Tron game that I referred to earlier. The more you move, the longer the list would be, and the longer it would take to determine whether or the object has collided with its own body.

We will try a slightly different approach, by using Surface.get_at, which tells us the color of the pixel at the given position. It actually returns (red, green, blue, alpha), but we won’t worry about the alpha part just yet.

The new code is below:


 1 #! /usr/bin/env python
 2
 3 # Move a worm across the screen. Beware of borders and self!
 4
 5 import pygame
 6
 7 class Worm:
 8     def __init__(self, surface, x, y, length):
 9         self.surface = surface
10         self.x = x
11         self.y = y
12         self.length = length
13         self.dir_x = 0
14         self.dir_y = -1
15         self.body = []
16         self.crashed = False
17
18     def key_event(self, event):
19         """ Handle keyboard events that affect the worm. """
20         if event.key == pygame.K_UP:
21             self.dir_x = 0
22             self.dir_y = -1
23         elif event.key == pygame.K_DOWN:
24             self.dir_x = 0
25             self.dir_y = 1
26         elif event.key == pygame.K_LEFT:
27             self.dir_x = -1
28             self.dir_y = 0
29         elif event.key == pygame.K_RIGHT:
30             self.dir_x = 1
31             self.dir_y = 0
32
33     def move(self):
34         """ Move the mouse. """
35         self.x += self.dir_x
36         self.y += self.dir_y
37
38         r, g, b, a = self.surface.get_at((self.x, self.y))
39         if (r, g, b) != (0, 0, 0):
40             self.crashed = True
41
42         self.body.insert(0, (self.x, self.y))
43
44         if len(self.body) > self.length:
45             self.body.pop()
46
47     def draw(self):
48         """ Draw the worm. """
49         for x, y in self.body:
50             self.surface.set_at((x, y), (255, 255, 255))
51
52 # Window dimensions.
53 width = 640
54 height = 400
55
56 screen = pygame.display.set_mode((width, height))
57 clock = pygame.time.Clock()
58 running = True
59
60 # Our worm.
61 w = Worm(screen, width/2, height/2, 200)
62
63 while running:
64     screen.fill((0, 0, 0))
65     w.draw()
66     w.move()
67
68     if w.crashed or w.x <= 0 or w.x >= width-1 or w.y <= 0 or w.y >= height-1:
69         print "Crash!!!"
70         running = False
71
72     for event in pygame.event.get():
73         if event.type == pygame.QUIT:
74             running = False
75         elif event.type == pygame.KEYDOWN:
76             w.key_event(event)
77
78     pygame.display.flip()
79     clock.tick(240)
80

As you can see, the new version is similar to the previous one for most part. Where it does differ is in the part where we detect whether or not the worm has collided with itself. We no longer need to compare the position of the head with each position of the body. Instead we simply compare the color of the pixel where the head is with the color of the background. If they don’t match then we have a collision, since the worm is the only object on the screen.

There is one other difference between the programs. In the event loop, look at the order in which the w.draw() and w.move() are in each program. Notice the difference? In order for the second program to be able to work at all, it is vital that we draw that worm before we move it. Think about it for a while and you will understand why.

Exercises

  1. Change the program to compare the pixel color at the worm’s head with the worm’s body color instead of the background color. If the color of the pixel is the same as that of the body it is a crash.
  2. Add obstacles around the screen. These should be randomly positioned pixels. Use a different color than the worm’s color. Add collision detection to determine if the worm has crashed onto one of the obstacles. If so, the program should exit.
  3. Give the worm food to eat. Food should be a pixel randomly positioned on the screen. Make it a different color from both obstacles and the worm itself. Any time the worm gets the food, the message “yummy” should be printed to the console and a new food should be generated. Keep a counter that increases every time the worm grabs a food item.

Optimizing

As a game developer, one thing you will have to get used to is spending hours with your code looking for places to optimize. What I’m going to do here is very trivial, but then again, we are working with a very trivial example.

One thing that we do that is really wasteful is “clear” the surface each frame. This wouldn’t be necessary if we were to instead plot a single pixel, the color of the background, at the very tail of the worm. The following program does that:


 1 #! /usr/bin/env python
 2
 3 # Move a worm around the screen. Beware of borders and self!
 4
 5 import pygame
 6
 7 class Worm:
 8     """ A worm. """
 9
10     def __init__(self, surface, x, y, length):
11         self.surface = surface
12         self.x = x
13         self.y = y
14         self.dx = 0
15         self.dy = -1
16         self.length = length
17         self.body = []
18         self.last = (0, 0)
19         self.crashed = False
20
21     def key_event(self, event):
22         """ Handle keyboard events that affect the worm. """
23         if event.key == pygame.K_UP:
24             self.dx = 0
25             self.dy = -1
26         elif event.key == pygame.K_DOWN:
27             self.dx = 0
28             self.dy = 1
29         elif event.key == pygame.K_LEFT:
30             self.dx = -1
31             self.dy = 0
32         elif event.key == pygame.K_RIGHT:
33             self.dx = 1
34             self.dy = 0
35
36     def move(self):
37         """ Move the mouse. """
38         self.body.insert(0, (self.x, self.y))
39         self.x += self.dx
40         self.y += self.dy
41
42         r, g, b, a = self.surface.get_at((self.x, self.y))
43
44         if (r, g, b) != (0, 0, 0):
45             self.crashed = True
46
47         if len(self.body) >= self.length:
48             self.last = self.body.pop()
49         else:
50             self.last = self.body[-1]
51
52     def draw(self):
53         """ Draw the worm """
54         self.surface.set_at((self.x, self.y), (255, 255, 255))
55         self.surface.set_at(self.last, (0, 0, 0))
56
57 # Window dimensions.
58 width = 640
59 height = 400
60
61 screen = pygame.display.set_mode((width, height))
62 screen.fill((0, 0, 0))
63 pygame.display.flip()
64
65 clock = pygame.time.Clock()
66 running = True
67
68 # Our worm.
69 w = Worm(screen, width/2, height/2, 200)
70
71 while running:
72     w.draw()
73     w.move()
74
75     if w.crashed or w.x <= 0 or w.x >= width-1 or w.y <= 0 or w.y >= height-1:
76         print "Crashed!"
77         running = False
78
79     for event in pygame.event.get():
80         if event.type == pygame.QUIT:
81             running = False
82         elif event.type == pygame.KEYDOWN:
83             w.key_event(event)
84
85     pygame.display.flip()
86     clock.tick(240)
87

Study the move() and draw() methods and compare them to the ones in the previous version. Also notice that there is no longer any screen.fill() call inside the event loop.

I have rearranged some of the code in the move() method. Is this necessary? Figure it out. You can always try rearranging lines to see what happens. You will learn much by experimenting with the code.

Exercises

  1. Analyze the last version of the worm program. How does it differ from the previous one? How does it work? In what ways can this said to be “optimized” as compared to the previous versions?
  2. Extend the worm program that draws food for the worm to eat in such a way that each time the worm eats food it grows a little longer.
  3. Extend the program in the previous exercise so that there is “poison mushroom” as well as food on the screen. If the worm eats one of these, it reduces in length. If the length goes below a certain minimum, the program should end.

Conclusion

That brings to end another pygame tutorial. I hope you enjoyed it. I know that I am moving way too slow for some people. There are loads of other good tutorials out there if you are in a hurry to learn. Many (all?) of them are far superior to my own.

Please do try and solve at least some of the exercises. If you want to send me your solutions or questions, you can find my email if you look around a bit. ;-)

2007.12.16

Pygame tutorial #5: pixels

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

Welcome to pygame tutorial #5. This time we’ll look at how to plot individual pixels. We will also look at a keyboard event that may prove useful when we get to the stage of actually being able to write games.

Setting a pixel

You can use Surface.set_at() to plot a single pixel on your surface. You will need to provide the pixel coordinates as well as the color you want to use. An example will (hopefully) make this clear.


 1 #! /usr/bin/env python
 2
 3 # Plot random pixels on the screen.
 4
 5 import pygame
 6 import random
 7
 8 # Window dimensions
 9 width = 640
10 height = 400
11
12 screen = pygame.display.set_mode((width, height))
13 clock = pygame.time.Clock()
14 running = True
15
16 while running:
17     x = random.randint(0, width-1)
18     y = random.randint(0, height-1)
19     red = random.randint(0, 255)
20     green = random.randint(0, 255)
21     blue = random.randint(0, 255)
22
23     screen.set_at((x, y), (red, green, blue))
24
25     for event in pygame.event.get():
26         if event.type == pygame.QUIT:
27             running = False
28
29     pygame.display.flip()
30     clock.tick(240)
31

This program just plots random pixels until the window is closed. It should be fairly straight-forward.

Exercises

  1. Modify the program above to take the window dimensions as command line arguments.
  2. Modify the program to start with a different background color.
  3. Write a program that contains a list of 16 preset colors. You can choose any colors you want, but red, green, blue, yellow, brown and purple are a few good ones. Create a program that plots pixels and random coordinates and uses random colors out of the preset list of colors only.
  4. Write a program that plots a pixel and the mouse position when the user presses the left mouse button.

Keydown events

You have learned how to deal with the QUIT event as well as MOUSEMOVE, MOUSEBUTTONDOWN and MOUSEBUTTONUP. This time let’s look at the KEYDOWN event. This even occurs when the user presses a key down.

When we recieve a KEYDOWN event, the event object will hold the code of the key that was pressed in an attribute called ‘key’. So by comparing event.key to whichever key we are interested in, we can find out if that was the actual key that was pressed. As usual, since I’m not good at explaining things, I hope the code below will make it clear.


 1 #! /usr/bin/env python
 2
 3 # Move a single pixel around the screen without crashing against the borders.
 4
 5 import pygame
 6
 7 # Window dimensions.
 8 width = 640
 9 height = 400
10
11 # Position of the pixel.
12 x = width / 2
13 y = height / 2
14
15 # Direction of the pixel.
16 dir_x = 0
17 dir_y = -1
18
19 screen = pygame.display.set_mode((width, height))
20 clock = pygame.time.Clock()
21 running = True
22
23 while running:
24     x += dir_x
25     y += dir_y
26
27     if x <= 0 or x >= width or y <= 0 or y >= height:
28         print "Crash!"
29         running = False
30
31     screen.fill((0, 0, 0))
32     screen.set_at((x, y), (255, 255, 255))
33     
34     for event in pygame.event.get():
35         if event.type == pygame.QUIT:
36             running = False
37         elif event.type == pygame.KEYDOWN:
38             if event.key == pygame.K_UP:
39                 dir_x = 0
40                 dir_y = -1
41             elif event.key == pygame.K_DOWN:
42                 dir_x = 0
43                 dir_y = 1
44             elif event.key == pygame.K_LEFT:
45                 dir_x = -1
46                 dir_y = 0
47             elif event.key == pygame.K_RIGHT:
48                 dir_x = 1
49                 dir_y = 0
50
51     pygame.display.flip()
52     clock.tick(120)

This program moves a pixel along the screen. Using the cursor keys you can change the direction of the pixel. If the pixel hits any of the borders the program terminates. Notice that keep a track not only of the x and y coordinates, but also the horizontal and vertical direction. When you need to keep track of several attributes of a single “thing” like this, that thing is definitely a candidate for being turned into a class.

In order to get used to moving “things” out into being classes, let’s take a look at an example of this. The following example looks and behaves just like the previous one, but there is a difference. We now create a class called MovingPixel. This class is responsible for keeping its own internal state (it’s current position, horizontal and vertical direction, etc). We could have added many more attributes to the class, but for the time being we keep things as simple as possible.

The class also contains a few methods in order for the main program to communicate to the class to perform certain actions, such as change direction, draw itself onto the specified surface and so on.

All in all, our program has grown slightly longer. But notice that the main program has grown a little less complex because we moved some things that are internal to the pixel into the MovingPixel class.

This example is obviouly very simplistic and I don’t necessarily recommend that you create classes to represent pixels (although to be fair, ours is a moving pixel). But the principle can be used on more complex objects that maybe require many drawing operations to draw. We will see more on these in future tutorials.


 1 #! /usr/bin/env python
 2
 3 # Move a single pixel around the screen without crashing against the borders.
 4
 5 import pygame
 6
 7 # These are used for directions.
 8 UP = (0, -1)
 9 DOWN = (0, 1)
10 LEFT = (-1, 0)
11 RIGHT = (1, 0)
12
13 class MovingPixel:
14     """ A moving pixel class. """
15
16     def __init__(self, x, y):
17         """ Creates a moving pixel. """
18         self.x = x
19         self.y = y
20         self.hdir = 0
21         self.vdir = -1
22
23     def direction(self, dir):
24         """ Changes the pixels direction. """
25         self.hdir, self.vdir = dir
26
27     def move(self):
28         """ Moves the pixel. """
29         self.x += self.hdir
30         self.y += self.vdir
31
32     def draw(self, surface):
33         surface.set_at((self.x, self.y), (255, 255, 255))
34
35 # Window dimensions.
36 width = 640
37 height = 400
38
39 screen = pygame.display.set_mode((width, height))
40 clock = pygame.time.Clock()
41 running = True
42
43 # Create a moving pixel.
44 pix = MovingPixel(width/2, height/2)
45
46 while running:
47     pix.move()
48
49     if pix.x <= 0 or pix.x >= width or pix.y <= 0 or pix.y >= height:
50         print "Crash!"
51         running = False
52
53     screen.fill((0, 0, 0))
54     pix.draw(screen)
55     
56     for event in pygame.event.get():
57         if event.type == pygame.QUIT:
58             running = False
59         elif event.type == pygame.KEYDOWN:
60             if event.key == pygame.K_UP:
61                 pix.direction(UP)
62             elif event.key == pygame.K_DOWN:
63                 pix.direction(DOWN)
64             elif event.key == pygame.K_LEFT:
65                 pix.direction(LEFT)
66             elif event.key == pygame.K_RIGHT:
67                 pix.direction(RIGHT)
68
69     pygame.display.flip()
70     clock.tick(120)

Exer