Coding Lunar Lander
Calvin Robinson uses Python to create three examples of the legendary Lunar Lander game – text-based, vector-based and complete with GUI.
Calvin Robinson uses Python to create three examples of the legendary Lunar
Lander game – text-based, vector-based and complete with GUI.
In this new Python series we’re going to be developing classic video games using contemporary techniques. This issue we’re kicking things off with the legendary Lunar Lander.
Lunar Lander games are a genre originating from the original Atari back in 1979, and are one of the oldest video game genres. The player controls a lander spacecraft and attempts to land the spacecraft by controlling the thrusters, while monitoring forces and fuel levels, with the game round ending in either a crash or a successful landing, most commonly the former. Points can be given for time and precision of landing.
The game world is black and white with vector graphics, displaying the environment and lander module in a 2D environment. We need to set up measurements for our lander’s fuel levels, speed and altitude. Our player will need a way of controlling the thrusters in upwards and left and right directions to steer the module.
Before we program the graphics it’s a good idea to get our head around the maths. For that reason, we’re going to program a text-based Lunar Lander first.
Let’s start by setting up some variables for our approach speed, gravity level, amount of fuel, altitude above the surface of the Moon, and initial burn rate. speed = 30;gravity = 1.622;fuel = 1500;altitude = 1000;burn = 0 Now we’ll ask the user for burn rates and calculate the speed and altitude accordingly, not forgetting to take into consideration gravity. If our pilot burns all the fuel the rocket will gravitate towards the Moon. while altitude > 0: if speed <= 0:
impact = 1000 else:
impact = altitude / speed print(“altitude={:8.3f} Speed={:6.3f} Fuel={:8.3f} Impact={:6.3f} Previous burn={:6.3f}”.format(altitude,sp eed,fuel,impact,burn))
burn = float(input(“enter an amount of fuel to burn between 0 and 50: “)) if burn < 0:
burn = 0 if burn > 50:
burn = 50 if burn > fuel:
burn = fuel altitude -= speed speed += gravity - burn/10 fuel -= burn
Specifying .3f and using .format{} enables us to be more precise with our output, sticking to three decimal places. Integers provide no decimal places, so they wouldn’t be helpful for this use case, while floats are generally inefficient for this level of calculation. We might see two decimal places for one number and three for another. To avoid inconsistencies we’ll set all printed data to display three numbers after the decimal point.
We’re running on a loop to get constant updates from our user and updating the flight information. We also have protections to ensure that the rate of fuel burning can’t exceed the amount of fuel remaining. This loop can conclude with two possible outcomes; either we crash our module or successfully land on the Moon. print(“altitude={:8.3f} Speed={:6.3f} Fuel={:8.3f} Last burn={:6.3f}”.format(altitude,speed,fuel,burn)) if altitude <- 5 or speed > 5:
print(“you have crashed.“) else:
print(“you have successfully landed.“)
We’ve set the altitude limit to 5, as our pilot should control the speed to below 5m/s before landing. If the module hits the surface with a speed greater than that, it’s classed as a crash. Likewise, if the lunar module attempts to land 5m or more below the surface, it will count as a crash. Anything else is considered a success.
Our text-based Lunar Lander is fun, but it’s time to take things to the next level by introducing some 2D vector graphics. Before we do, we’ll need to ensure our Python setup includes a few modules. Python can be a little tricky, so make sure you’ve only got one version of Python 3 installed, or set up a virtual environment with
virtualenv. Install Pygame and Pyaudio:
$ Pip3 install pygame
$ Pip3 install pyaudio
If you experience difficulties installing Pyaudio, as we did, you may have to install it manually. First install portaudio, then grab the source code for Pyaudio:
$ sudo apt-get install portaudio19-dev
$ wget Pyaudio-0.2.11.tar.gz
$ tar -zxvf Pyaudio-0.2.11.tar.gz
$ cd Pyaudio
$ sudo python3 setup.py install
Assuming everything is working, we may begin by including these modules in our new Python file:
import pygame, sys, pyaudio, array from random import * from pygame import * from math import *
Next, initiate the variables similar to the text-based version. We’ll need a game loop to keep things running, and set up events for each key press. We’ll use the Left and Right arrow keys for the appropriate side thrusters and Down to control the vertical thruster. We’ll keep our display strings short (Fuel, Alt, Vertical and Horizontal Speed) in line with the small window size.
for i in range(mn+1):
mx.append(z*i);my.append(int(randint(mh,0)+am*(4-sin((i+ph)/5.)))-fs) mx.append(s);my.append(randint(smh,s));mx[pl]=mx[pl-1]+lp;my[pl]=my[pl-1] while dn == False: for event in pygame.event.get(): if event.type==quit:dn=true if event.type==keydown: if event.key==k_escape:dn=true if event. key==k_r:x=randint(z,z);y=z;u=v=0;r=5;cg=w;wi=n;f=s;gs=c if event.key==k_down and f>0:v=v-a;f=f-5;cl=w;cr =w;ss=se if event.key==k_left and f>0:u=u+a;f=f5;cl=w;ss=se if event.key==k_right and f>0:u=u-a;f=f5;cr=w;ss=se if gs==c and (x<0 or x>s):x=x-(abs(x)/x)*s if gs==c:v=v+1;x=(10*x+u)/10;y=(10*y+v)/10 if (y+8)>=my[pl] and x>mx[pl-1] and x
... //see code file TXT=’FUEL %3d ALT %3d VERT SPD %3d HORZ
SPD %3d’%(f,s-y,v,u) sp=ft.render(txt,0,w);sc.blit(sp,(0,s12));cl=b;cr=b;stream.write(ss) for i in range(mn):draw.line(sc,w,(mx[i],my[i]),(mx[i+ 1],my[i+1])) sp=ft.render(gs,0,w);sc.blit(sp,(s/3,s/2));display. flip();clk.tick(5);ss=c
Don’t forget to close everything off at the end with a termination, quit and close:
pygame.quit();stream.close();pa.terminate()
Having created a text-based lunar lander, followed by a basic visual game with Pygame, let’s design one with a graphical user interface. This time we have far fewer modules to import, but we need to initiate our variables:
from tkinter import * import time
GRAVITY = 0.0005
ENGINE_POWER = -0.00003 THRUSTER_POWER = 0.00001 MAXIMUM_ENGINE_POWER = -0.002 MAXIMUM_THRUSTER_POWER = 0.0001
We’re using tkinter as our GUI module, as it’s quite accessible as far as Python GUIS go. To use tkinter properly we’ll need to develop this game using objectoriented programming. OOP provides a class/object structure that makes adapting and updating the game so much easier in the long run.
class Game: def __init__(self): self.tk = Tk() self.tk.title(“lander”) self.tk.resizable(0, 0) self.tk.wm_attributes(“-topmost”, 1) self.canvas = Canvas(self.tk, width=700, height=500, highlightthickness=0) self.canvas.pack() self.canvas.focus_set() self.tk.update() self.canvas_height = 500 self.canvas_width = 1000 self.sprites = [] self.running = True def mainloop(self): while 1: if self.running == True: for sprite in self.sprites:
sprite.move() self.tk.update_idletasks() self.tk.update() time.sleep(0.01)
Our primary class Game has two functions, __init__ and mainloop. The former initiates everything we’ll need to use setting up our game window; Title is the window title, canvas the variables for the dimensions of that window. Pack is a tkinter command to create the window, and focus_set makes sure this window takes focus. The latter function, mainloop, is the main loop that ensures everything keeps running. Here, it continuously displays our sprites. Create a class for our
Lunar Lander, with an init function to boot: class Lander: def __init__(self, game): self.canvas = game.canvas self.id = self.canvas.create_rectangle(340, 0, 360, 20) self.time = time.time() self.x = 0 self.y = 0.01 self.engine_y = 0 self.engine_x = 0 self.fuel = 2000 self.down = False self.left = False self.right = False game.canvas.bind_all(‘
This is our first class with an argument parameter (self, game) , meaning we can assign an object of our Game class. We’ve set up some object variables, too. Canvas stores the game’s canvas details we set up; ID
is the unique identifier of our lunar lander module, which in this case is a rectangle for the sake of simplicity; Time will monitor the current time, once we’ve set it up; X and Y are movement values to control our rectangle; Engine_x and Engine_y will control the horizontal and vertical engine power respectively; Down, Left and Right will be toggles for our engine, so we know if the thrusters are active or not, depending on when the keys are pressed. We could have just as easily used WASD. The _text options display the current variables on our display so that we know how the engine and thrusters are currently toggled, and how much fuel remains. To do this, we set up a function for each: def engine_down(self, evt): if self.down: self.down = False self.engine_y = 0 self.canvas.itemconfig(self.engine_y_text, text=’main Engine: off’) else: self.down = True self.canvas.itemconfig(self.engine_y_text, text=’main Engine: on’) def engine_left(self, evt): if self.right or self.left: self.left = False self.right = False self.engine_x = 0 self.canvas.itemconfig(self.engine_x_text, text=’thrusters: off’) else: self.left = True self.canvas.itemconfig(self.engine_x_text, text=’thrusters: left’)
...
Since we’re toggling the engine thrusters on/off this time, we’ll need to use Boolean logic, setting variables to True/false to control the movement of our lander module. We’ll set the variable to True when the appropriate function is active and False when it’s not. Pressing the buttons we assigned in the previous function will activate/deactivate these Booleans. We’ll also change the canvas text we set up earlier to reflect the new status of our thrusters and/or engine.
Finally, we need to design a function to actually implement the movement, following these button presses and activations:
def move(self): if self.canvas.coords(self.id)[3] >= 500: if self.y > 0.5:
print(’ You have crashed.‘) else:
print(’ You have successfully landed.‘) return now = time.time() time_since_last = now - self.time if time_since_last > 0.1:
if self.down and self.engine_y > MAXIMUM_
ENGINE_POWER: self.engine_y += ENGINE_POWER if self.left and self.engine_x < MAXIMUM_
THRUSTER_POWER: self.engine_x += THRUSTER_POWER elif self.right and self.engine_x > -MAXIMUM_
THRUSTER_POWER:
self.engine_x -= THRUSTER_POWER if self.down:
self.fuel -= 2 if self.left or self.right:
self.fuel -= 1 if self.fuel < 0:
self.fuel = 0 self.canvas.itemconfig(self.engine_fuel_text, text=’fuel: %s’ % self.fuel) if self.fuel <= 0: self.engine_y = 0 self.engine_x = 0 self.y = self.y + (time_since_last * GRAVITY) + (time_since_last * self.engine_y) self.x = self.x + (time_since_last * self.engine_x) self.canvas.move(self.id, self.x, self.y)
We’ve implemented some very basic collision detection. Like the last version of our game, if you come in too fast you’re going to crash, but if you land on the target with a speed of 5m/s or less you’ll win the game.
We store the time in a variable called now. We can use this to calculate the time difference between actions taking place. We can subtract now from time and see how long something took. We have an if
function set up to see if time is moving (by measuring if it has changed by more than a tenth of a second).
Here we monitor key presses and work out our engine power. When our engine reaches the maximum engine power or maximum thruster power we add or subtract to it, accordingly. For example, if the value in our engine_y variable is greater than maximum_ engine_power we’ll add engine_power to engine_y.
That means if we launch the game and tap the Right key, our program will need to add a value to engine_y as
soon as Right becomes True, because engine_y started on 0. That’s how we get some movement started with Down, Left or Right. The same would be the case with engine_x and maximum_thruster_power.
As soon as we begin the movement calculations we’ll also need to take into account the amount of fuel being burnt. The moment Down, Left, Right are pressed we’re giving the engine more power and emptying the fuel reserves. We’re subtracting by two each time. Throughout all of this we’re updating our on-screen text to display the latest values from our variables.
Towards the end of the move function we’ve put a logic safeguard in place. If the fuel ever reaches a number below zero we reset it back to zero. If/when the fuel tank teaches zero we turn our engines and thrusters off, switching engine_y and engine_x to 0. Then we work out the new position of the lunar lander. To find our x value we take our time_since_last value to calculate the time taken since the function was last run and multiply it against engine_x, before adding it to our x
value. This gives the illusion of momentum, as our lunar lander will move even after shutting off the power. For our y value we also need to take gravity into consideration, which is why we also multiply Gravity by time_since_last before adding it to y. After each calculation we update our display with canvas.move to put these changes into effect. Our final class is the Platform. cwlass Platform: def __init__(self, game): self.canvas = game.canvas self.id = self.canvas.create_rectangle(600, 480, 650, 490)
Finally, create new instances of our objects to spawn in our game world and display as sprites on our screen. g = Game() lander = Lander(g) platform = Platform(g) g.sprites.append(lander) g.mainloop()
Experiment, extend and generally play with the code!