lander/notebook.txt
David Handy
http://www.handysoftware.com/cpif/

This file contains my project notes for the Lander game case study, as
described in the book (see the web site above.)

Session 1
---------
Saturday, 19 June 2004

To practice what I preach, I'm following the 7 personal project management
steps for success.

1. Begin with the end in mind

I have a pretty good idea of what I want to make, because I have seen it
before: A lunar-lander style game where your spaceship starts out some
distance above the surface (of some planet or the moon or whatever) and
begins to fall, and you use the arrow keys to fire rockets on the bottom and
sides to guide your space craft and slow its descent to a (hopefully) safe
landing.

I have made this kind of game before. This time I want to use the cpif package
for the graphics and sound effects.

My goals are to impress my children by making a version of the lander game with
sound effects, and to provide a real example of a sucessful personal
programming project to the readers of my beginning programming book.

2. Write it down

I just did that - see above.

3. Don't try to eat the whole whale at once

I am pretty confident that this project is within the scope of my time and
abilities, because of my Python experience and my experience making similar
programs. Also, I wrote the cpif package that I am using for the graphics
and sound.

If I were an absolute beginner starting a project like this, I would plan on
spending extra time figuring out Python and getting used to using the cpif
package. But that might be worth it for the learning experience.

4. Plan to impress someone

I plan to impress my children, and also provide an example for the readers
of my book. Because so many other people are involved in this project, I am
very motivated to complete it.

5. Set a date

I have a publication date in mind for the book, and this lander game must be
finished long before that. I have set a goal to be done with this game (and
this notebook of my work on the game) by the end of June 2004. I have
committed to this date, in writing, to others.

6. Keep your memory

I created a lander directory that has everything related to this lander game
project. This notebook.txt file and all of the Python program files are in
that directory.

7. Use Python

Yes, absolutely!


Now that I have started out right with the 7 steps for success, I'll make a
first plan and put it into my todo.txt file:

    To start, just write a program that creates a window, and quits when Esc
    is pressed.  Next, draw the lander craft in the middle of the window.

I'll write this code and call it lander1.py. You can find lander1.py in the
lander directory in your MyPrograms directory, if cpif is installed
completely on your computer.

I drew in my paper notebook a simple shape for a spaceship, and figured out
the coordinates of the points based on a size variable that I could change
later. Here is what it looks like in "ASCII art":

            +-----------------------------------------+
            |                                         |
            |                                         |
            | y - size/2 ....... 1                    |
            |                                         |
            |                                         |
            |                                         |
            | y   .............  *                    |
            |                                         |
            |                    3                    |
            |                    .                    |
            | y + size/2 ....4   .   2                |
            |                .   .   .                |
            |                .   .   .                |
            |                .   .   .                |
            |                .   .   .                |
            |       x - size/4   x   x + size/4       |
            |                                         |
            |                                         |
            |                                         |
            +-----------------------------------------+

Imagine a line going from 1 to 2, then from 2 to 3, then 3 to 4, and finally
back to 1. This forms the outline of a polygon. Imagine the polygon filled
in with blue. That's what the spaceship will look like. The * is the center
of the spaceship, which has the coordinates x, y. I created the drawShip()
function to draw the spaceship, with x and y as the parameters. I use
dw.polygon() to do the actual drawing.

In my book I said that you should write down mistakes in your notebook so
you don't repeat them. Here's one mistake: when I ran my first version of
lander1.py, I didn't see a spaceship in the middle of the window, but
instead saw fragments of a spaceship in the upper-left corner. I suspected
something was wrong with the code I used to get the width and height of the
window:

    width, height = dw.getSize()

Normally, that would be Ok, but I put that code *before* calling dw.run(),
therefore the window is not yet visible. Since the window is not yet
visible, what are its width and height? I put the following statement in the
program to find out:

    print width, height

and it printed:

    1 1

Oops! I decided it was best to store the width and height I used when
creating the window, like this:

    width, height = 640, 480
    dw = DrawingWindow(width, height)

That fixed the problem. Now I have a blue spaceship drawn in the center of
my window.

That's all I have time for in this session. The lander1.py file in your
lander directory is the final result of today's work. In the future you will
have a lander2.py, lander3.py, etc. in the lander directory in MyPrograms
that shows what the program looks like at the end of each session.


Session 2
---------
Tuesday 22 June 2004

What shall I do today? I replaced the text in todo.txt with the following:

    Add gravity, so that the ship begins to fall.

That actually involves a lot of things, since the ship must now change
position with time. But I think I can (barely) do that in the time I have
this morning before I head off to my day job.

I start by copying lander1.py to lander2.py and changing that file.

Next, I created a class called SpaceShip. The __init__ method of SpaceShip
takes startx and starty parameters for the starting position of the ship.
__init__ sets the initial position and velocity of the ship and draws the
ship, saving the drawing ID.

I changed keyCallback() and drawShip() from being functions to being methods
of the SpaceShip class (therefore I added a self parameter to each
function.)

I added another method, update(). It moves the position of the ship by
self.vx and self.vy (the velocity) and also changes self.vy, the velocity in
the up-down direction, by the gravity factor (which I'll bet back to later.)

In main(), I changed the variable ship to be an instance of SpaceShip(), got
rid of the call to drawShip(), and changed the callback passed to
dw.onKeyDownCall to ship.keyCallback.

Most importantly, I added the timer call:

    dw.onTimerCall(0.05, ship.update)

This updates the position of the ship every 0.05 seconds, or in other words
20 times a second. I came up with that number by trial and error, by running
lander2.py again and seeing what the biggest number was that still resulted
in smooth animation.

The other number I tweaked by trial and error was the gravity factor in the
SpaceShip update() method. My final version was:

        self.vy = self.vy + 0.07

The 0.07 number is the gravity factor. Make it higher, the ship falls
faster.

That's all for today. I'll make a plan for tomorrow and put it in my
todo.txt file:

    Draw rocket flames when the arrow buttons are pressed.
    If there is time, also have the rockets accelerate the space ship.

See lander2.py in your lander directory for the final result of this
session.

Please notice that at the end of each session, I try to have have one new
small feature working completely, instead of several new features that don't
work. Trying to do too much at one time will cause you to get bogged down
and possibly not complete your project, or not have something working by
your deadline.


Session 3
---------
Wednesday 23 June 2004

Following the plan I made yesterday, I'm adding the drawing of rocket flame
when I press the down, left, or right arrow keys.

This involves figuring out the coordinates of the flame outline, similar to
how I figured out the coordinates of the corners of the space ship.


     +----------------------------------------------------+
     |                                                    |
     |                               x + size/4           |
     |                               .                    |
     |                               .   x + size/2       |
     |                           ^   .   .                |
     |                               .   .   x + size*.75 |
     |  y - size/4......(2)          .  (2)  .            |
     |                               .       .            |
     |  y ..........(3)     (1)  *  (1)     (3)           |
     |                                                    |
     |  y + size/4......(4)      ^      (4)               |
     |                                                    |
     |                       v       v                    |
     |                                                    |
     |                                                    |
     |                                                    |
     +----------------------------------------------------+

In the ASCII-art diagram above the ^ and v characters are the corners of the
space ship, and (1), (2), (3), and (4) are the corners of the left and right
flame burst polygons. The * is the center of the space ship, at coordinates
(x, y).

The down flame will be a polygon a little larger than the others:


            +-----------------------------------------+
            |                                         |
            |                    ^                    |
            |                                         |  
            |                                         |
            |                                         |
            |                    *                    |
            |                                         |
            |                    ^                    |
            |                                         |
            | y + size...... v  (1)  v                |
            |                                         |
            |                                         |
            |                                         |
            | y + size*1.5..(4)     (2)               |
            |                .       .                |
            |                .       .                |
            |                .       .                |
            | y + size*2....    (3)  .                |
            |                .   .   .                |
            |                .   .   .                |
            |     x - size*.25   x   x + size*.25     |
            |                                         |
            |                                         |
            +-----------------------------------------+

I'll create a drawSideFlame() method on SpaceShip that takes a direction
parameter, which will be -1 for left and 1 for right. This will allow me to
easily reflect the coordinates of the right flame to turn them into the
coordinates for the left flame. I'll create a drawDownFlame() for the other
flame burst.

Next I'll change the name of keyCallback() to keyDownCallback(), and add
another method, keyUpCallback(). If we don't register a method using
dw.onKeyUpCall(), we won't be able to turn the flame off!

Finally I'll add if statements to keyDownCallback() and keyUpCallback() to
check which key is being pressed (using event.keysym, as described in the
book) and draw or erase the appropriate flame. I'll also add to the update
method, to move the flames as well as the ship. That's probably all I'll be
able to get to today.

I had to do some trial-and-error on the flame coordinates to get them to look
right. Before changing the key callback methods, I first tested the flame
drawing methods by temporarily putting the following lines at the end of the
SpaceShip.__init__ method:

        self.drawSideFlame(1)
        self.drawSideFlame(-1)
        self.drawDownFlame()

This causes the ship to be drawn with all flames on. Only when all the flames
looked right did I go on to work on the key callback methods. Try to do one
thing at a time!

In SpaceShip.keyDownCallback, I check if event.keysym equals 'Down', and
turn on the down flame. I'll  do the same for 'Left' and 'Right' after I get
the down flame working. The draw flame methods set an attribute (i.e.
self.right_flame) to be the ID of the flame polygon. These attributes were
all set to None in __init__().

In SpaceShip.keyUpCallback, I check if event.keysym equals 'Down', and if
the flame attribute (self.down_flame) is not None, I delete it. I do the
same for 'Right' and 'Left', after I get the down flame working.

The first time I tried it, I got the logic for key up and key down reversed,
so that pressing the key turned off the rocket, and releasing it turned it
on. Oops. But it was easily fixed.

Ok, I got the left, right, and side flames to draw correctly, and hooked
them up to the left, right, and down arrow keys, respectively. But the space
ship just keeps on falling! That's a problem for another day, I'm out of
time now.

The result of today's work is in lander3.py in your lander directory.


Session 4
---------
Thursday 24 June 2004

I updated my todo.txt file with my next tasks:

    Make the rockets accelerate the space ship.
    Make the space ship land or crash.

Making the space ship accelerate involves some Physics, although we will take
some shortcuts today, as I will explain.

Acceleration means change in velocity. The place where I change the space
ship's velocity is in the update() method.

In real life as rockets burn up fuel, the space ship loses mass, and that
causes the acceleration due to the rocket's force to increase. But I'm going
to ignore that fact for simplification purposes right now, and have the
acceleration due to the rockets be a constant - a smaller constant for the
side rockets, since they are smaller.

Falling due to gravity is also acceleration.  In real life the acceleration
changes depending on how far away you are from the planet, but close to the
planet's surface it is nearly constant. We'll use a constant for the
acceleration due to gravity in this game.

I changed the last few lines in the update() method to look like this:

        # acceleration
        gravity = 0.07
        side_rocket_accel = 0.01
        down_rocket_accel = 0.09
        if self.right_flame:
            self.vx = self.vx - side_rocket_accel
        if self.left_flame:
            self.vx = self.vx + side_rocket_acce
        if self.down_flame:
            self.vy = self.vy - down_rocket_accel
        self.vy = self.vy + gravity

Notice that if the right_flame is turned on, I subtract from self.vx, and if
the left_flame is turned on, I add to self.vx. That's because subtracting
from the x coordinate moves left, and a rocket pushing on the right will
move you left. It may take you a little while to get used to pushing the
right arrow key to move the rocket left!

Likewise, gravity adds to self.vy because y coordinates increase going
downward. The down_flame boosts the ship upward.

I think I got the code right. I'll run it and see.

Well, I found at least one problem:

NameError: global name 'side_rocket_acce' is not defined

I'll fix the typo and try again.

I would like to slow down the game temporarily so I can better see how the
space ship reacts when I press the arrow keys. I added this line near the
top of the file:

REFRESH_INTERVAL = 0.05 # seconds between updates

Then I changed my call to  onTimerCall to look like this:

    dw.onTimerCall(REFRESH_INTERVAL, ship.update)

Now I can easily change the refresh interval if I so desire.

After trying out the rocket engines, I found they were too weak. So I
increased the rocket boost factor variables, after some trial and error, to:

        side_rocket_accel = 0.03
        down_rocket_accel = 0.14

This session's result is in lander4.py.

Session 5
---------
Friday, 25 June 2004

Now I can control the space ship's rockets with the arrow keys, and the
rockets accelerate the space ship, but the space ship never lands!

Today I'll try to make it so that the space ship lands if it touches down
softly at the bottom of the window, and crashes if it hits too hard. Either
way, I'll put some text above the space ship explaining what happened, and stop
the game until the user presses R to restart.

One thing we are missing is any instructions on how to play the game. Since
we're adding text in this session anyway, I'll start by putting some text at
the top of the screen telling the user that they can use the arrow keys to
control the rockets. I'll do this in the __init__ method.

I'll create an attribute called 'landed' that will be None at the beginning,
and will contain the ID of the text drawn when the space ship lands.

I'll create another attribute called 'crashed' that will be None at the
beginning, and will contain the ID of the text drawn if the space ship
crashes.

I'll modify the update() method to check the speed and position of the space
ship, and decide if it has crashed or landed, and display the appropriate
text. If the space ship has crashed or landed already, update should do
nothing, since the space ship should not be moving.

Detecting if the ship has landed means knowning if the bottom of the ship
has reached the bottom of the window. I'll use the self.size atribute to
figure out the distance from the coordinates of the ship to the bottom of
the ship, and use dw.getSize() to figure out the current size of the window
(this is Ok because the window is fully drawn now, so dw.getSize() will
return the correct values.)

The keyDownCallBack() method needs to detect the R key and restart the game.
Restarting the game means setting the position of the space ship back to the
beginning (I need to store the beginning position so I can do that), setting
the x and y velocity to zero, and deleting the landed and crashed text if
any.

Ok, I think that is all the changes I'll have to make. I'll give it a try.

Oops - I made a mistake. I'll share it with you, since it is a pretty common
thing for me to do. I created a new method on SpaceShip called
drawTextAboveShip(), but when I called it I forgot to write

    self.drawTextAboveShip("You crashed!")

but instead said:

    drawTextAboveShip("You crashed!")

This made Python think I meant to call a function instead of a method on
that same crash, and it gave me an exception that said:

    NameError: global name 'drawTextAboveShip' is not defined

Another thing I forgot to do, which is fairly common, is to put a self
parameter on my drawTextAboveShip method. After I "fixed" it, my
drawTextAboveShip() method looked like this:

    def drawTextAboveShip(self, message):
        """
        Draw text centered above the top of the space ship.
        Return the id of the newly-created text.
        """
        text_height, text_width = dw.getTextSize(message)
        text_x = self.x - (text_width / 2)
        text_y = self.y - self.size - text_height
        id = dw.drawText(text_x, text_y, message)
        return id

Can you see the major bug in this method? Neither could I, at first. But my
text was being drawn in crazy places, instead of above the ship where I
wanted it. Finally I put this line in the code, right before the call to
drawText():

        print "text_x =", text_x, "text_y =", text_y

This is called a "debug print statement". When I used drawTextAboveShip() to
print the first message right when the game started, here is what it
printed:

text_x = 314 text_y = -60

That text_x coordinate looks Ok, but -60 is just crazy - it is out of sight
above the top of the window. So I added more debug print statements:

        print "self.y =", self.y, "self.size =", self.size,
        print "text_height = ", text_height

and I got this result:

self.y = 240 self.size = 20 text_height =  280

That's crazy. 280 pixels would make more sense for the text width, not the
text height. Does the computer think the text is standing on its head?
Finally I noticed the problem: I had mistaken the order of the values
returned by dw.getTextSize(). It returns (width, height), not the other way
around. So I changed the line to:

        text_width, text_height = dw.getTextSize(message)

and the text started appearing in the right place.

The reason I shared all of these details with you is so I could:

1) Give you a real example of how to debug a problem

2) Let you know that even programmers with 20 years experience make mistakes
   like this, so you shouldn't feel discouraged as a beginner if you make
   "dumb" mistakes too.

Please note that I haven't put *all* of my boneheaded mistakes in this
notebook, just a representative sample. I deleted my debug print statments
when I was done with them.

The landing, crashing, and restarting seems to be working Ok. It was a
little too hard to get a safe landing, so I bumped the crash speed up to:

        crash_speed = 0.50

Great! Now we have a working lander game. You can find the result in
lander5.py in your lander directory.

I still want sound effects.  That will be the task for the next and final
session.


Session 6
Saturday, 25 June 2004

I did a little preparation for this session by recording some sounds. I have
put these sounds in the cpif package for anybody to use. They are
thrust1.wav, which I will play when one of the side rockets is activated,
thrurst2.wav, which I will play when the down rocket is activated, and
boom.wav, which I will play if the space ship crashes.

Playing sounds at the right time turned out to be trickier than I thought.
I'm glad I devoted a whole session just to the sound effects. At first I
thought I could just put a line like this in keyDownCallback(), for example:

        elif event.keysym == 'Down':
            if not self.down_flame:
NEW LINE -->    self.playSound('thrust2.wav')
                self.drawDownFlame()

This doesn't work, however, because when I hold press and hold the down
arrow key, my computer rapidly generates a series of key down and key up
messages. This causes the sound to be played a dozen times a second - only
the sound doesn't finish that quickly. So a whole bunch of sounds get lined
up to play, and keep on playing long after I've released the down arrow key,
which is real annoying.

The solution is to play the rocket sounds in the update() method, only if
there is not a sound already playing. I modified the playSound() method to
create and save a sound object, and in update() call that sound's
isPlaying() method to see if it is done yet.

The final touch, the cherry on top, was to use the cpif.music module to play
a couple of notes when the game starts or is restarted:

        start_sound = music.soundOfMusic([('E4', 0.125), ('C4', 0.125)])
        start_sound.play()

and to play a little tune if you land safely:

                land_music = [('C4', 0.125), ('D4', 0.125), ('E4', 0.125), 
                              ('E5', 0.25), ('C5', 0.125)]
                land_sound = music.soundOfMusic(land_music)
                land_sound.play()

And that's all, folks. You can find the result in lander6.py in your lander
directory. My boys were impressed (at least, they played with it a while),
so I declare the project a success.

What, you want more? You want a black background, with stars, and little
green men walking around on the surface, and a target to land on, and keep
score, and run out of fuel if you take too long, and... Well, as my college
math teachers use to always say, that's an exercise for the student. Have
fun!

end-of-file
