Python lunar landing game

Have your Apollo 11 moment: A Python Lunar Landing game using the turtle module

How difficult can it be to land on the moon? I have no idea. All I know is that it’s not that easy landing the lunar module in this Python turtle game:

But, how about writing the game? I’ve taken a ‘first-principles’ approach to write this lunar landing game and used Python’s turtle module instead of other game-writing libraries.

Let me take you all the way from launch to landing.

The Python Lunar Landing Game

Look at the video of the game again. The lunar module starts in a spot in the top-left corner of the game screen. It is also spinning with a random angular velocity at the beginning of the game.

The aim of the game is to land the lunar module safely on the landing pad by controlling its descent.

Controlling the Lunar Module

You can turn on either of the two thrusters or both at once. If only one thruster is turned on, the module’s rotational velocity increases. This affects how fast the lunar module is spinning and in which direction. The longer the thruster is on, the faster it will spin.

If the module is spinning clockwise, let’s say, and the anticlockwise (counterclockwise) thruster is turned on and kept on, the lunar module’s spinning will slow down until it stops rotating completely for a brief period. Then, it will start spinning anticlockwise.

If both thrusters are turned on at the same time, the lunar module will accelerate in the direction opposite to where the thrusters are facing. If the module is spinning and both thrusters are turned on, the direction of acceleration will keep changing as the module spins. This makes the module hard to control when it’s spinning rapidly!

Landing the Lunar Module

The lunar module must land on the landing pad while facing upwards. There is some tolerance level that is acceptable on both location of landing and orientation of the lunar module when it reaches the landing pad.

However, if the lunar module hits the landing pad outside of these tolerances, the landing is unsuccessful. The landing is considered a failed landing also if the lunar module goes below the bottom edge of the game screen.

Setting Up the Scene

You’ll use Python’s turtle module to create the graphics in this game. If you’ve used this module before, you’re already familiar with the key classes you’ll use and the methods to draw and move things around.

However, it’s not a problem if you’ve never used the turtle module. I’ll introduce everything that’s needed from this module as and when required in this article.

Creating the Game Window

You can start by creating the window you’ll need for the game and setting its size and background colour:

import turtle

# Set up the game window
window = turtle.Screen()
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

turtle.done()

The first step is to create an object representing the screen in which the game will run. You name it window. When you call window.setup(0.6, 0.6), you set the size of the window to 60% of your display’s width and height. You can also use integers as arguments in setup() to choose the pixel size directly instead of choosing the fraction of your display’s width and height.

You’ll need to use the actual width and height of the window often throughout the code, so you assign the values returned by window_width() and window_height() to width and height.

The remaining methods set the window’s title bar and the background colour. Finally, turtle.done() keeps the program from exiting and keeps the window open. This is the mainloop of the game when using turtle.

Creating the Stars and the Moon’s Surface

The other key object available in the turtle module is the Turtle object. This is the drawing pen which you’ll be able to move around the screen to draw things. You can create two “turtles” to create the stars and the moon:

import random
import turtle

# Set up the game window
window = turtle.Screen()
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

# Game parameters
n_of_stars = 100

# Create stars and moon
stars = turtle.Turtle()
stars.hideturtle()
stars.penup()
stars.color("white")
for _ in range(n_of_stars):
    # Use floor division // to ensure ints in randint()
    x_pos = random.randint(-width // 2, width // 2)
    y_pos = random.randint(-height // 2, height // 2)
    stars.setposition(x_pos, y_pos)
    stars.dot(random.randint(2, 6))

moon = turtle.Turtle()
moon.penup()
moon.color("slate gray")
moon.sety(-height * 2.8)
moon.dot(height * 5)

turtle.done()

You use several Turtle methods:

  • hideturtle() hides the arrow representing the Turtle object on the screen.
  • penup() ensures that as you move the turtle around the screen, no lines are drawn.
  • color() sets the turtle’s colour and that of the graphics it produces.
  • dot() draws a dot with any given size.
  • setposition() places the turtle at a given set of coordinates.
  • setx() and sety() set only one of the coordinates, either x or y.

You’ve now set up the background for this Python lunar landing game:

Lunar Landing game - background

However, you’ll have noticed that it takes a long time for the turtles to move around, drawing all the stars and the moon. The turtle module draws each small step the turtles make. This takes time. This issue will also cause lag during the gameplay as every movement will be slowed down due to the program drawing every step of every movement.

You can turn this default behaviour off by calling the window.tracer(0), which does not draw any of the intermediate steps. The screen is refreshed each time you call window.update():

import random
import turtle

# Set up the game window
window = turtle.Screen()
window.tracer(0)
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

# Game parameters
n_of_stars = 100

# Create stars and moon
stars = turtle.Turtle()
stars.hideturtle()
stars.penup()
stars.color("white")
for _ in range(n_of_stars):
    # Use floor division // to ensure ints in randint()
    x_pos = random.randint(-width // 2, width // 2)
    y_pos = random.randint(-height // 2, height // 2)
    stars.setposition(x_pos, y_pos)
    stars.dot(random.randint(2, 6))

moon = turtle.Turtle()
moon.penup()
moon.color("slate gray")
moon.sety(-height * 2.8)
moon.dot(height * 5)

window.update()
turtle.done()

That completes the background of the lunar landing game. Now, for the fun part!

Creating the Lunar Module

Next, you need to draw the lunar module. However, the spaceship is not stationary in this Python lunar landing game. It spins and it moves around as the player tries to land it:

import random
import turtle

# Set up the game window
window = turtle.Screen()
window.tracer(0)
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

# Game parameters
n_of_stars = 100
# Lunar module design parameters
branch_size = width / 16
n_of_discs = 5
disc_colour = "light gray"
centre_colour = "gold"
landing_gear_colour = "red"

# Create stars and moon
stars = turtle.Turtle()
stars.hideturtle()
stars.penup()
stars.color("white")
for _ in range(n_of_stars):
    # Use floor division // to ensure ints in randint()
    x_pos = random.randint(-width // 2, width // 2)
    y_pos = random.randint(-height // 2, height // 2)
    stars.setposition(x_pos, y_pos)
    stars.dot(random.randint(2, 6))

moon = turtle.Turtle()
moon.penup()
moon.color("slate gray")
moon.sety(-height * 2.8)
moon.dot(height * 5)

# Create the lunar module
lunar_module = turtle.Turtle()
lunar_module.penup()
lunar_module.hideturtle()
lunar_module.setposition(-width / 3, height / 3)

def draw_lunar_module():
    lunar_module.pendown()
    lunar_module.pensize(5)
    # Landing gear
    lunar_module.color(landing_gear_colour)
    lunar_module.forward(branch_size)
    lunar_module.left(90)
    lunar_module.forward(branch_size / 2)
    lunar_module.forward(-branch_size)
    lunar_module.forward(branch_size / 2)
    lunar_module.right(90)
    lunar_module.forward(-branch_size)
    lunar_module.pensize(1)
    # Pods around the edge of the module
    lunar_module.color(disc_colour)
    for _ in range(n_of_discs - 1):
        lunar_module.right(360 / n_of_discs)
        lunar_module.forward(branch_size)
        lunar_module.dot(branch_size / 2)
        lunar_module.forward(-branch_size)
    # Centre part of the lunar module
    lunar_module.color(centre_colour)
    lunar_module.dot(branch_size)
    lunar_module.penup()

# Will remove this later
draw_lunar_module()

window.update()
turtle.done()

You add parameters to define the size and colours of the lunar module and create a new Turtle object lunar_module. The position of this turtle is in the top-left region of the window.

Then, you define draw_lunar_module(), which does what the function name says! You can read through the steps in the function to follow the lunar_module turtle as it draws the landing gear, the outer pods and the central part of the lunar module. The variable branch_size determines the distance between the centre of the lunar module and the centre of one of the outer discs. You’ll use this value to scale several aspects of the drawing.

You add a temporary call to draw_lunar_module() so you can look at what the lunar module looks like:

Lunar Landing game in turtle – spaceship

There is one problem that you can’t see yet but will soon become evident. Try and add a second call to draw_lunar_module() immediately after the one already in the code:

lunar landing issue with drawing second frame of lunar module

As the lunar_module turtle moves around to draw the spaceship, it ends up in the same place it starts, which is at the centre of the spaceship but facing in a different orientation. Therefore, when you draw the lunar module a second time, it’s facing the wrong direction.

You can work out the maths you need to make sure the turtle ends its drawing of the lunar module facing the same way as when it started. However, there’s an easier solution:

import random
import turtle

# Set up the game window
window = turtle.Screen()
window.tracer(0)
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

# Game parameters
n_of_stars = 100
# Lunar module design parameters
branch_size = width / 16
n_of_discs = 5
disc_colour = "light gray"
centre_colour = "gold"
landing_gear_colour = "red"

# Create stars and moon
stars = turtle.Turtle()
stars.hideturtle()
stars.penup()
stars.color("white")
for _ in range(n_of_stars):
    # Use floor division // to ensure ints in randint()
    x_pos = random.randint(-width // 2, width // 2)
    y_pos = random.randint(-height // 2, height // 2)
    stars.setposition(x_pos, y_pos)
    stars.dot(random.randint(2, 6))

moon = turtle.Turtle()
moon.penup()
moon.color("slate gray")
moon.sety(-height * 2.8)
moon.dot(height * 5)

# Create the lunar module
lunar_module = turtle.Turtle()
lunar_module.penup()
lunar_module.hideturtle()
lunar_module.setposition(-width / 3, height / 3)

def draw_lunar_module():
    # "save" the starting position and orientation
    position = lunar_module.position()
    heading = lunar_module.heading()

    lunar_module.pendown()
    lunar_module.pensize(5)
    # Landing gear
    lunar_module.color(landing_gear_colour)
    lunar_module.forward(branch_size)
    lunar_module.left(90)
    lunar_module.forward(branch_size / 2)
    lunar_module.forward(-branch_size)
    lunar_module.forward(branch_size / 2)
    lunar_module.right(90)
    lunar_module.forward(-branch_size)
    lunar_module.pensize(1)
    # Pods around the edge of the module
    lunar_module.color(disc_colour)
    for _ in range(n_of_discs - 1):
        lunar_module.right(360 / n_of_discs)
        lunar_module.forward(branch_size)
        lunar_module.dot(branch_size / 2)
        lunar_module.forward(-branch_size)
    # Centre part of the lunar module
    lunar_module.color(centre_colour)
    lunar_module.dot(branch_size)
    lunar_module.penup()

    # reset the turtle to initial position and orientation
    lunar_module.setposition(position)
    lunar_module.setheading(heading)

# Will remove this later
print(lunar_module.heading())
print(lunar_module.position())
draw_lunar_module()
draw_lunar_module()
print(lunar_module.heading())
print(lunar_module.position())

window.update()
turtle.done()

You start the definition of draw_lunar_module() by storing the position and heading of the turtle before it starts drawing. Then,you finish the function definition by resetting the turtle’s position and orientation. You don’t really need to reset the position as the turtle is already in the correct location. However, you may need to do this if you go for a different spaceship design!

In the last few lines of code, you print out the lunar module’s orientation and position before and after you call draw_lunar_module() twice to confirm that these remain the same after successive calls to the function.

Now, you can remove the lines in the # Will remove this later section now.

Adding Thrusters to Turn the Lunar Module

There are two key things you’ll need to do to add thrusters that can turn the lunar module. There’s the “artistic” side of showing the burning fuel coming out of the thrusters and the “functional” side that turns the lunar module. Let’s start with the latter.

You can start by creating an instance variable bound to lunar_module called rotation which determines the rotational speed of the lunar module. For the time being, you can set this to 0.

You create two more instance variables which are also bound to lunar_module. These instance variables determine whether the clockwise and anticlockwise thrusters are on or off. Initially, you set these to False, which means the thrusters are turned off. Then, you define two functions which can turn these thrusters on.

This is a good time to create the main game loop. All the steps needed in every frame of the animation will occur in the while loop.

Although you can set a required number of frames per second to make sure your game runs at a specific frame rate, I’m choosing a simpler version in this project in which we just let the while loop run at whichever speed it will run without controlling its exact timing. However, you can add a short delay to each while loop to slow it down if it’s running too fast. The sleep() function from the time module is useful for this:

import random
import time
import turtle

# Set up the game window
window = turtle.Screen()
window.tracer(0)
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

# Game parameters
n_of_stars = 100
# Lunar module design parameters
branch_size = width / 16
n_of_discs = 5
disc_colour = "light gray"
centre_colour = "gold"
landing_gear_colour = "red"
# Lunar module movement parameters
rotation_step = 0.2

# Create stars and moon
stars = turtle.Turtle()
stars.hideturtle()
stars.penup()
stars.color("white")
for _ in range(n_of_stars):
    # Use floor division // to ensure ints in randint()
    x_pos = random.randint(-width // 2, width // 2)
    y_pos = random.randint(-height // 2, height // 2)
    stars.setposition(x_pos, y_pos)
    stars.dot(random.randint(2, 6))

moon = turtle.Turtle()
moon.penup()
moon.color("slate gray")
moon.sety(-height * 2.8)
moon.dot(height * 5)

# Create the lunar module
lunar_module = turtle.Turtle()
lunar_module.penup()
lunar_module.hideturtle()
lunar_module.setposition(-width / 3, height / 3)
lunar_module.rotation = 0
lunar_module.clockwise_thruster = False
lunar_module.anticlockwise_thruster = False

def draw_lunar_module():
    lunar_module.clear()
    # "save" the starting position and orientation
    position = lunar_module.position()
    heading = lunar_module.heading()

    lunar_module.pendown()
    lunar_module.pensize(5)
    # Landing gear
    lunar_module.color(landing_gear_colour)
    lunar_module.forward(branch_size)
    lunar_module.left(90)
    lunar_module.forward(branch_size / 2)
    lunar_module.forward(-branch_size)
    lunar_module.forward(branch_size / 2)
    lunar_module.right(90)
    lunar_module.forward(-branch_size)
    lunar_module.pensize(1)
    # Pods around the edge of the module
    lunar_module.color(disc_colour)
    for _ in range(n_of_discs - 1):
        lunar_module.right(360 / n_of_discs)
        lunar_module.forward(branch_size)
        lunar_module.dot(branch_size / 2)
        lunar_module.forward(-branch_size)
    # Centre part of the lunar module
    lunar_module.color(centre_colour)
    lunar_module.dot(branch_size)
    lunar_module.penup()

    # reset the turtle to initial position and orientation
    lunar_module.setposition(position)
    lunar_module.setheading(heading)

def turn_on_clockwise_thruster():
    lunar_module.clockwise_thruster = True

def turn_on_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = True

window.onkeypress(turn_on_clockwise_thruster, "Right")
window.onkeypress(turn_on_anticlockwise_thruster, "Left")
window.listen()

while True:
    # Change rotational speed of lunar module
    if lunar_module.clockwise_thruster:
        lunar_module.rotation -= rotation_step
    if lunar_module.anticlockwise_thruster:
        lunar_module.rotation += rotation_step
    # Rotate lunar module
    lunar_module.left(lunar_module.rotation)

    # Refresh image of lunar module
    draw_lunar_module()

    time.sleep(0.05)
    window.update()

turtle.done()

You add a call to lunar_module.clear() at the beginning of draw_lunar_module() so that each time you redraw the spaceship, the previous drawing is cleared from the screen.

You bind the functions turn_on_clockwise_thruster() and turn_on_anticlockwise_thruster() to the right and left arrow keys using window.onkeypress() and window.listen(). The latter method ensures that the program is “listening” out for keypresses.

This works. However, there’s a problem:

The rotational speed of the lunar module keeps increasing once you hit the arrow key. That’s because you can toggle the thruster on, but you can’t turn it off.

You can amend this by adding a couple more functions to turn the thrusters off. I’m only showing a section of the code below. The rest of the code is unchanged:

# ...

def turn_on_clockwise_thruster():
    lunar_module.clockwise_thruster = True

def turn_on_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = True

def turn_off_clockwise_thruster():
    lunar_module.clockwise_thruster = False

def turn_off_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = False

window.onkeypress(turn_on_clockwise_thruster, "Right")
window.onkeypress(turn_on_anticlockwise_thruster, "Left")
window.onkeyrelease(turn_off_clockwise_thruster, "Right")
window.onkeyrelease(turn_off_anticlockwise_thruster, "Left")
window.listen()

# ...

Releasing the arrow keys now turns the thrusters off. Therefore, you have more control over how the lunar module spins:

You can control the animation’s speed by changing the argument in time.sleep() if you need.

Drawing the Burning Fuel

You can now change the rotational speed of the lunar module by using the arrow keys. Next, you can focus on the “artistic” element of turning the thrusters on and off. You can create another Turtle object and a function to draw the burning fuel:

import random
import time
import turtle

# Set up the game window
window = turtle.Screen()
window.tracer(0)
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

# Game parameters
n_of_stars = 100
# Lunar module design parameters
branch_size = width / 16
n_of_discs = 5
disc_colour = "light gray"
centre_colour = "gold"
landing_gear_colour = "red"
# Lunar module movement parameters
rotation_step = 0.2

# Create stars and moon
stars = turtle.Turtle()
stars.hideturtle()
stars.penup()
stars.color("white")
for _ in range(n_of_stars):
    # Use floor division // to ensure ints in randint()
    x_pos = random.randint(-width // 2, width // 2)
    y_pos = random.randint(-height // 2, height // 2)
    stars.setposition(x_pos, y_pos)
    stars.dot(random.randint(2, 6))

moon = turtle.Turtle()
moon.penup()
moon.color("slate gray")
moon.sety(-height * 2.8)
moon.dot(height * 5)

# Create the lunar module
lunar_module = turtle.Turtle()
lunar_module.penup()
lunar_module.hideturtle()
lunar_module.setposition(-width / 3, height / 3)
lunar_module.rotation = 0
lunar_module.clockwise_thruster = False
lunar_module.anticlockwise_thruster = False

def draw_lunar_module():
    lunar_module.clear()
    # "save" the starting position and orientation
    position = lunar_module.position()
    heading = lunar_module.heading()

    lunar_module.pendown()
    lunar_module.pensize(5)
    # Landing gear
    lunar_module.color(landing_gear_colour)
    lunar_module.forward(branch_size)
    lunar_module.left(90)
    lunar_module.forward(branch_size / 2)
    lunar_module.forward(-branch_size)
    lunar_module.forward(branch_size / 2)
    lunar_module.right(90)
    lunar_module.forward(-branch_size)
    lunar_module.pensize(1)
    # Pods around the edge of the module
    lunar_module.color(disc_colour)
    for _ in range(n_of_discs - 1):
        lunar_module.right(360 / n_of_discs)
        lunar_module.forward(branch_size)
        lunar_module.dot(branch_size / 2)
        lunar_module.forward(-branch_size)
    # Centre part of the lunar module
    lunar_module.color(centre_colour)
    lunar_module.dot(branch_size)
    lunar_module.penup()

    # reset the turtle to initial position and orientation
    lunar_module.setposition(position)
    lunar_module.setheading(heading)

# Create burning fuel
burning_fuel = turtle.Turtle()
burning_fuel.penup()
burning_fuel.hideturtle()

def draw_burning_fuel(thruster):
    # Place turtle in the correct location
    # depending on which thruster is on
    if thruster == "clockwise":
        direction = 1
    elif thruster == "anticlockwise":
        direction = -1
    burning_fuel.penup()
    burning_fuel.setposition(lunar_module.position())
    burning_fuel.setheading(lunar_module.heading())
    burning_fuel.right(direction * 360 / n_of_discs)
    burning_fuel.forward(branch_size)
    burning_fuel.left(direction * 360 / n_of_discs)

    # Draw burning fuel
    burning_fuel.pendown()
    burning_fuel.pensize(8)
    burning_fuel.color("yellow")
    burning_fuel.forward(branch_size)
    burning_fuel.backward(branch_size)
    burning_fuel.left(5)
    burning_fuel.color("red")
    burning_fuel.pensize(5)
    for _ in range(2):
        burning_fuel.forward(branch_size)
        burning_fuel.backward(branch_size)
        burning_fuel.right(10)

def turn_on_clockwise_thruster():
    lunar_module.clockwise_thruster = True

def turn_on_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = True

def turn_off_clockwise_thruster():
    lunar_module.clockwise_thruster = False

def turn_off_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = False

window.onkeypress(turn_on_clockwise_thruster, "Right")
window.onkeypress(turn_on_anticlockwise_thruster, "Left")
window.onkeyrelease(turn_off_clockwise_thruster, "Right")
window.onkeyrelease(turn_off_anticlockwise_thruster, "Left")
window.listen()

while True:
    burning_fuel.clear()
    # Change rotational speed of lunar module
    if lunar_module.clockwise_thruster:
        draw_burning_fuel("clockwise")
        lunar_module.rotation -= rotation_step
    if lunar_module.anticlockwise_thruster:
        draw_burning_fuel("anticlockwise")
        lunar_module.rotation += rotation_step
    # Rotate lunar module
    lunar_module.left(lunar_module.rotation)

    # Refresh image of lunar module
    draw_lunar_module()

    time.sleep(0.05)
    window.update()

turtle.done()

In the draw_burning_fuel() function definition, you’re moving the turtle to the centre of one of the outer discs. The lunar_module turtle’s resting position is at the centre of the spaceship, facing downwards towards the landing gear. You use the direction variable, which is either 1 or -1, to send the turtle to the correct disc.

The drawing of the burning fuel is simply three thick lines: a yellow one in the middle and two red ones on either side of the middle yellow line!

You can now set the initial rotation to a random value since you need the initial parameters to be random to make the game more challenging:

# ...

# Create the lunar module
lunar_module = turtle.Turtle()
lunar_module.penup()
lunar_module.hideturtle()
lunar_module.setposition(-width / 3, height / 3)
lunar_module.rotation = random.randint(-9, 9)
lunar_module.clockwise_thruster = False
lunar_module.anticlockwise_thruster = False

# ...

Each time you run the program, the lunar module will start off spinning with a random rotational velocity. You can practise controlling the rotation of the lunar module using the thrusters before moving on!

Moving the Lunar Module

You can rotate the lunar module by turning either thruster on and off. However, rotational movement is only one of the ways the lunar module can move in this Python lunar landing game. Now, you need to be able to translate the lunar module, too.

There are two factors that will make the lunar module move from its starting location: gravity and thrust. When both thrusters are turned on at the same time, the lunar module will be pushed forward in the direction it’s facing. Gravity, on the other hand, will act on the lunar module all the time.

You can add two instance variables bound to lunar_module called travel_speed and travel_direction. These instance variables determine the lunar module’s speed and direction of travel at any time in the animation. Note that the orientation of the turtle used to draw the spaceship is not the same as the direction of travel of the spaceship:

# ...

# Create the lunar module
lunar_module = turtle.Turtle()
lunar_module.penup()
lunar_module.hideturtle()
lunar_module.setposition(-width / 3, height / 3)
lunar_module.rotation = random.randint(-9, 9)
lunar_module.clockwise_thruster = False
lunar_module.anticlockwise_thruster = False
lunar_module.travel_speed = random.randint(1, 3)
lunar_module.travel_direction = random.randint(-45, 0)

# ...

You set both of the new instance variables you created to random values so that the lunar module’s starting position is different each time you run the game. Next, you need to move the lunar module using these values. Therefore, you can add a section in the while loop that works out how much the spaceship should move in the x- and y-directions and move it:

import math
import random
import time
import turtle

# Set up the game window
window = turtle.Screen()
window.tracer(0)
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

# Game parameters
n_of_stars = 100
# Lunar module design parameters
branch_size = width / 16
n_of_discs = 5
disc_colour = "light gray"
centre_colour = "gold"
landing_gear_colour = "red"
# Lunar module movement parameters
rotation_step = 0.2

# Create stars and moon
stars = turtle.Turtle()
stars.hideturtle()
stars.penup()
stars.color("white")
for _ in range(n_of_stars):
    # Use floor division // to ensure ints in randint()
    x_pos = random.randint(-width // 2, width // 2)
    y_pos = random.randint(-height // 2, height // 2)
    stars.setposition(x_pos, y_pos)
    stars.dot(random.randint(2, 6))

moon = turtle.Turtle()
moon.penup()
moon.color("slate gray")
moon.sety(-height * 2.8)
moon.dot(height * 5)

# Create the lunar module
lunar_module = turtle.Turtle()
lunar_module.penup()
lunar_module.hideturtle()
lunar_module.setposition(-width / 3, height / 3)
lunar_module.rotation = random.randint(-9, 9)
lunar_module.clockwise_thruster = False
lunar_module.anticlockwise_thruster = False
lunar_module.travel_speed = random.randint(1, 3)
lunar_module.travel_direction = random.randint(-45, 0)

def draw_lunar_module():
    lunar_module.clear()
    # "save" the starting position and orientation
    position = lunar_module.position()
    heading = lunar_module.heading()

    lunar_module.pendown()
    lunar_module.pensize(5)
    # Landing gear
    lunar_module.color(landing_gear_colour)
    lunar_module.forward(branch_size)
    lunar_module.left(90)
    lunar_module.forward(branch_size / 2)
    lunar_module.forward(-branch_size)
    lunar_module.forward(branch_size / 2)
    lunar_module.right(90)
    lunar_module.forward(-branch_size)
    lunar_module.pensize(1)
    # Pods around the edge of the module
    lunar_module.color(disc_colour)
    for _ in range(n_of_discs - 1):
        lunar_module.right(360 / n_of_discs)
        lunar_module.forward(branch_size)
        lunar_module.dot(branch_size / 2)
        lunar_module.forward(-branch_size)
    # Centre part of the lunar module
    lunar_module.color(centre_colour)
    lunar_module.dot(branch_size)
    lunar_module.penup()

    # reset the turtle to initial position and orientation
    lunar_module.setposition(position)
    lunar_module.setheading(heading)

# Create burning fuel
burning_fuel = turtle.Turtle()
burning_fuel.penup()
burning_fuel.hideturtle()

def draw_burning_fuel(thruster):
    # Place turtle in the correct location
    # depending on which thruster is on
    if thruster == "clockwise":
        direction = 1
    elif thruster == "anticlockwise":
        direction = -1
    burning_fuel.penup()
    burning_fuel.setposition(lunar_module.position())
    burning_fuel.setheading(lunar_module.heading())
    burning_fuel.right(direction * 360 / n_of_discs)
    burning_fuel.forward(branch_size)
    burning_fuel.left(direction * 360 / n_of_discs)

    # Draw burning fuel
    burning_fuel.pendown()
    burning_fuel.pensize(8)
    burning_fuel.color("yellow")
    burning_fuel.forward(branch_size)
    burning_fuel.backward(branch_size)
    burning_fuel.left(5)
    burning_fuel.color("red")
    burning_fuel.pensize(5)
    for _ in range(2):
        burning_fuel.forward(branch_size)
        burning_fuel.backward(branch_size)
        burning_fuel.right(10)

def turn_on_clockwise_thruster():
    lunar_module.clockwise_thruster = True

def turn_on_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = True

def turn_off_clockwise_thruster():
    lunar_module.clockwise_thruster = False

def turn_off_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = False

window.onkeypress(turn_on_clockwise_thruster, "Right")
window.onkeypress(turn_on_anticlockwise_thruster, "Left")
window.onkeyrelease(turn_off_clockwise_thruster, "Right")
window.onkeyrelease(turn_off_anticlockwise_thruster, "Left")
window.listen()

while True:
    burning_fuel.clear()
    # Change rotational speed of lunar module
    if lunar_module.clockwise_thruster:
        draw_burning_fuel("clockwise")
        lunar_module.rotation -= rotation_step
    if lunar_module.anticlockwise_thruster:
        draw_burning_fuel("anticlockwise")
        lunar_module.rotation += rotation_step
    # Rotate lunar module
    lunar_module.left(lunar_module.rotation)

    # Translate lunar module
    x = lunar_module.travel_speed * math.cos(
        math.radians(lunar_module.travel_direction)
    )
    y = lunar_module.travel_speed * math.sin(
        math.radians(lunar_module.travel_direction)
    )
    lunar_module.setx(lunar_module.xcor() + x)
    lunar_module.sety(lunar_module.ycor() + y)

    # Refresh image of lunar module
    draw_lunar_module()

    time.sleep(0.05)
    window.update()

turtle.done()

You calculate the x- and y-components of the lunar module’s speed using trigonometry. You import the math module, too. Then, you can shift the position of the lunar_module turtle accordingly.

When you run this code, the lunar module will start travelling at the speed and direction determined by the random values chosen at the beginning. You can change the rotational speed of the lunar module using the thrusters:

However, you still cannot change the lunar module’s speed and direction of travel.

Some Maths

Let’s review the maths that you’ll need to work out the change in the lunar module’s speed and direction when a force acts on it. Consider the lunar module that’s travelling in the direction shown by the green arrow below:

Python lunar landing maths

If the thrusters are turned on, they will create a force pointing in the direction shown by the red arrow in the diagram above. This direction represents the top of the lunar module.

You can break this force vector into two components, which are shown as blue arrows in the diagram:

  • the tangential component of the force created by the thrusters is the component that acts in the same direction as the spaceship’s current direction of travel. This is the blue arrow that’s pointing in the same direction as the green arrow.
  • the normal component of the force is the component that acts perpendicularly to the spaceship’s current direction of travel. This is shown as the blue arrow that’s 90º to the green arrow.

You can calculate the thruster force’s tangential and normal components by multiplying the magnitude of the force by the cosine and sine of the angle between the direction of the force and the direction of travel.

Turning On Both Thrusters

You can start by creating speed_step, which determines the step size by which you increase the speed each time you apply a “unit” of force. You also define apply_force(), which works out the change in direction and speed needed for each “unit” of thruster force applied. The function is called once in each iteration of the while loop when both thrusters are turned on:

# ...

# Game parameters
n_of_stars = 100
# Lunar module design parameters
branch_size = width / 16
n_of_discs = 5
disc_colour = "light gray"
centre_colour = "gold"
landing_gear_colour = "red"
# Lunar module movement parameters
rotation_step = 0.2
speed_step = 0.1

# ...

window.onkeypress(turn_on_clockwise_thruster, "Right")
window.onkeypress(turn_on_anticlockwise_thruster, "Left")
window.onkeyrelease(turn_off_clockwise_thruster, "Right")
window.onkeyrelease(turn_off_anticlockwise_thruster, "Left")
window.listen()

# Applying forces to translate the lunar module
def apply_force():
    # Initial components of lunar module velocity
    tangential = lunar_module.travel_speed
    normal = 0

    force_direction = lunar_module.heading() + 180

    angle = math.radians(
        force_direction - lunar_module.travel_direction
    )

    # New components of lunar module velocity
    tangential += speed_step * math.cos(angle)
    normal += speed_step * math.sin(angle)

    direction_change = math.degrees(
        math.atan2(normal, tangential)
    )
    lunar_module.travel_direction += direction_change

    lunar_module.travel_speed = math.sqrt(
        normal ** 2 + tangential ** 2
    )

while True:
    burning_fuel.clear()
    # Apply thrust if both thrusters are on
    if (
        lunar_module.clockwise_thruster
        and lunar_module.anticlockwise_thruster
    ):
        apply_force()
    # Change rotational speed of lunar module
    if lunar_module.clockwise_thruster:
        draw_burning_fuel("clockwise")
        lunar_module.rotation -= rotation_step
    if lunar_module.anticlockwise_thruster:
        draw_burning_fuel("anticlockwise")
        lunar_module.rotation += rotation_step
    # Rotate lunar module
    lunar_module.left(lunar_module.rotation)

    # Translate lunar module
    x = lunar_module.travel_speed * math.cos(
        math.radians(lunar_module.travel_direction)
    )
    y = lunar_module.travel_speed * math.sin(
        math.radians(lunar_module.travel_direction)
    )
    lunar_module.setx(lunar_module.xcor() + x)
    lunar_module.sety(lunar_module.ycor() + y)

    # Refresh image of lunar module
    draw_lunar_module()

    time.sleep(0.05)
    window.update()

turtle.done()

In apply_force(), you start by setting the tangential component of the velocity to the current speed of the lunar module. The normal component is 0 at this point. That’s because the tangential component is along the spaceship’s direction of travel.

Since the turtle drawing the lunar module faces the bottom of the lunar module in its “resting” state, you can set the direction of the force to the opposite direction of this by adding 180º. The turtle module measures angles in degrees. However, when using sines and cosines, you’ll need to convert these to radians.

Next, you can break down the change in speed from one iteration into its tangential and normal components and add them to the starting tangential and normal components of the lunar module’s velocity.

Now that you have the new components, you can work out the new speed and direction of the spaceship. You also add an if statement in the while loop to call apply_force() whenever both thrusters are turned on.

You can now fully steer the lunar module by:

  • turning thrusters on one at a time to change the lunar module’s rotation, or
  • turning both thrusters on at the same time to change the lunar module’s velocity.

The last thing that you’ll need to make the spaceship’s movement more realistic is to add the effect of gravity on the lunar module.

Adding the Effects of Gravity

In this game, we can assume a constant value for the moon’s gravitational pull on the spaceship. You create a variable called gravity to define this value. You can fine-tune this and other initial values to change the game’s difficulty level if you wish.

The force due to gravity is similar to the force applied when both thrusters are turned on. The only differences are the magnitude of the force and the direction. Gravity always pulls the lunar module vertically downwards.

This means that you don’t need to write a new function to take gravity into account. You can re-use apply_force() and make some modifications:

# ...

# Game parameters
n_of_stars = 100
# Lunar module design parameters
branch_size = width / 16
n_of_discs = 5
disc_colour = "light gray"
centre_colour = "gold"
landing_gear_colour = "red"
# Lunar module movement parameters
rotation_step = 0.2
speed_step = 0.1

gravity = 0.03

# ...

# Applying forces to translate the lunar module
def apply_force(mode):
    # Initial components of lunar module velocity
    tangential = lunar_module.travel_speed
    normal = 0

    if mode == "gravity":
        force_direction = -90
        step = gravity
    elif mode == "thrusters":
        force_direction = lunar_module.heading() + 180
        step = speed_step

    angle = math.radians(
        force_direction - lunar_module.travel_direction
    )

    # New components of lunar module velocity
    tangential += step * math.cos(angle)
    normal += step * math.sin(angle)

    direction_change = math.degrees(
        math.atan2(normal, tangential)
    )
    lunar_module.travel_direction += direction_change

    lunar_module.travel_speed = math.sqrt(
        normal ** 2 + tangential ** 2
    )

while True:
    burning_fuel.clear()
    # Apply thrust if both thrusters are on
    if (
        lunar_module.clockwise_thruster
        and lunar_module.anticlockwise_thruster
    ):
        apply_force("thrusters")
    # Change rotational speed of lunar module
    if lunar_module.clockwise_thruster:
        draw_burning_fuel("clockwise")
        lunar_module.rotation -= rotation_step
    if lunar_module.anticlockwise_thruster:
        draw_burning_fuel("anticlockwise")
        lunar_module.rotation += rotation_step
    # Rotate lunar module
    lunar_module.left(lunar_module.rotation)

    # Apply effect of gravity
    apply_force("gravity")

    # Translate lunar module
    x = lunar_module.travel_speed * math.cos(
        math.radians(lunar_module.travel_direction)
    )
    y = lunar_module.travel_speed * math.sin(
        math.radians(lunar_module.travel_direction)
    )
    lunar_module.setx(lunar_module.xcor() + x)
    lunar_module.sety(lunar_module.ycor() + y)

    # Refresh image of lunar module
    draw_lunar_module()

    time.sleep(0.05)
    window.update()

turtle.done()

You refactor apply_force() by adding a required argument. This argument will be either "gravity" or "thrusters", depending on which function mode you need to use. Note that you’ll need to update the call to apply_force(), which you already have in your code, to include the "thrusters" argument.

You also refactor the function to use the new local variable step as the change in speed you need to apply to the lunar module. When using gravity-mode, this value is equal to the gravity value, and the direction is -90º, which is vertically downwards. When using thrusters-mode for this function, the step and direction values are the same as they were before this last set of changes.

You also need to call apply_force("gravity") in each iteration of the while loop since gravity will always act on the lunar module.

The spaceship will start falling and accelerating towards the lunar surface when you run the program now. You’ll need to control its spinning and use the thrusters to push the lunar module back up:

You’ve now completed the part of this Python lunar landing program which controls the movement of the lunar module. Once you’ve practised your piloting skills, you’re ready to work on landing the spaceship!

Landing the Lunar Module

It’s time to land your lunar module in this Python lunar landing game. You’ll first need to create the landing pad on the moon’s surface. You also need to define acceptable tolerances for successfully landing the lunar module. Next, you’ll need a function that checks whether there has been a successful landing or not:

import math
import random
import time
import turtle

# Set up the game window
window = turtle.Screen()
window.tracer(0)
window.setup(0.6, 0.6)
window.title("The Python Lunar Landing Game")
window.bgcolor("black")

width = window.window_width()
height = window.window_height()

# Game parameters
n_of_stars = 100
# Lunar module design parameters
branch_size = width / 16
n_of_discs = 5
disc_colour = "light gray"
centre_colour = "gold"
landing_gear_colour = "red"
# Lunar module movement parameters
rotation_step = 0.2
speed_step = 0.1
# Landing parameters
landing_pad_position = 0, -height / 2.1
module_landing_position = (
    landing_pad_position[0],
    landing_pad_position[1] + branch_size,
)
landing_pos_tolerance_x = 20
landing_pos_tolerance_y = 5
landing_orientation = 270  # vertically downwards
landing_orientation_tolerance = 15

gravity = 0.03

# Create stars and moon
stars = turtle.Turtle()
stars.hideturtle()
stars.penup()
stars.color("white")
for _ in range(n_of_stars):
    # Use floor division // to ensure ints in randint()
    x_pos = random.randint(-width // 2, width // 2)
    y_pos = random.randint(-height // 2, height // 2)
    stars.setposition(x_pos, y_pos)
    stars.dot(random.randint(2, 6))

moon = turtle.Turtle()
moon.penup()
moon.color("slate gray")
moon.sety(-height * 2.8)
moon.dot(height * 5)

# Create landing pad
landing_pad = turtle.Turtle()
landing_pad.hideturtle()
landing_pad.penup()
landing_pad.setposition(landing_pad_position)
landing_pad.pendown()
landing_pad.pensize(10)
landing_pad.forward(branch_size / 2)
landing_pad.forward(-branch_size)
landing_pad.forward(branch_size / 2)

# Create the lunar module
lunar_module = turtle.Turtle()
lunar_module.penup()
lunar_module.hideturtle()
lunar_module.setposition(-width / 3, height / 3)
lunar_module.rotation = random.randint(-9, 9)
lunar_module.clockwise_thruster = False
lunar_module.anticlockwise_thruster = False
lunar_module.travel_speed = random.randint(1, 3)
lunar_module.travel_direction = random.randint(-45, 0)

def draw_lunar_module():
    lunar_module.clear()
    # "save" the starting position and orientation
    position = lunar_module.position()
    heading = lunar_module.heading()

    lunar_module.pendown()
    lunar_module.pensize(5)
    # Landing gear
    lunar_module.color(landing_gear_colour)
    lunar_module.forward(branch_size)
    lunar_module.left(90)
    lunar_module.forward(branch_size / 2)
    lunar_module.forward(-branch_size)
    lunar_module.forward(branch_size / 2)
    lunar_module.right(90)
    lunar_module.forward(-branch_size)
    lunar_module.pensize(1)
    # Pods around the edge of the module
    lunar_module.color(disc_colour)
    for _ in range(n_of_discs - 1):
        lunar_module.right(360 / n_of_discs)
        lunar_module.forward(branch_size)
        lunar_module.dot(branch_size / 2)
        lunar_module.forward(-branch_size)
    # Centre part of the lunar module
    lunar_module.color(centre_colour)
    lunar_module.dot(branch_size)
    lunar_module.penup()

    # reset the turtle to initial position and orientation
    lunar_module.setposition(position)
    lunar_module.setheading(heading)

# Create burning fuel
burning_fuel = turtle.Turtle()
burning_fuel.penup()
burning_fuel.hideturtle()

def draw_burning_fuel(thruster):
    # Place turtle in the correct location
    # depending on which thruster is on
    if thruster == "clockwise":
        direction = 1
    elif thruster == "anticlockwise":
        direction = -1
    burning_fuel.penup()
    burning_fuel.setposition(lunar_module.position())
    burning_fuel.setheading(lunar_module.heading())
    burning_fuel.right(direction * 360 / n_of_discs)
    burning_fuel.forward(branch_size)
    burning_fuel.left(direction * 360 / n_of_discs)

    # Draw burning fuel
    burning_fuel.pendown()
    burning_fuel.pensize(8)
    burning_fuel.color("yellow")
    burning_fuel.forward(branch_size)
    burning_fuel.backward(branch_size)
    burning_fuel.left(5)
    burning_fuel.color("red")
    burning_fuel.pensize(5)
    for _ in range(2):
        burning_fuel.forward(branch_size)
        burning_fuel.backward(branch_size)
        burning_fuel.right(10)

def turn_on_clockwise_thruster():
    lunar_module.clockwise_thruster = True

def turn_on_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = True

def turn_off_clockwise_thruster():
    lunar_module.clockwise_thruster = False

def turn_off_anticlockwise_thruster():
    lunar_module.anticlockwise_thruster = False

window.onkeypress(turn_on_clockwise_thruster, "Right")
window.onkeypress(turn_on_anticlockwise_thruster, "Left")
window.onkeyrelease(turn_off_clockwise_thruster, "Right")
window.onkeyrelease(turn_off_anticlockwise_thruster, "Left")
window.listen()

# Applying forces to translate the lunar module
def apply_force(mode):
    # Initial components of lunar module velocity
    tangential = lunar_module.travel_speed
    normal = 0

    if mode == "gravity":
        force_direction = -90
        step = gravity
    elif mode == "thrusters":
        force_direction = lunar_module.heading() + 180
        step = speed_step

    angle = math.radians(
        force_direction - lunar_module.travel_direction
    )

    # New components of lunar module velocity
    tangential += step * math.cos(angle)
    normal += step * math.sin(angle)

    direction_change = math.degrees(
        math.atan2(normal, tangential)
    )
    lunar_module.travel_direction += direction_change

    lunar_module.travel_speed = math.sqrt(
        normal ** 2 + tangential ** 2
    )

# Check for successful landing
def check_landing():
    if (
        abs(lunar_module.xcor() - module_landing_position[0])
        < landing_pos_tolerance_x
        and abs(lunar_module.ycor() - module_landing_position[1])
        < landing_pos_tolerance_y
    ):
        if (
            abs(lunar_module.heading() - landing_orientation)
            < landing_orientation_tolerance
        ):
            lunar_module.setposition(module_landing_position)
            lunar_module.setheading(landing_orientation)
            draw_lunar_module()
            burning_fuel.clear()
            return True
        else:
            burning_fuel.clear()
            return False  # Crash on landing pad - wrong angle
    if lunar_module.ycor() < -height / 2:
        burning_fuel.clear()
        return False  # Crash below moon surface
    return None  # No successful or unsuccessful landing yet

while True:
    burning_fuel.clear()
    # Apply thrust if both thrusters are on
    if (
        lunar_module.clockwise_thruster
        and lunar_module.anticlockwise_thruster
    ):
        apply_force("thrusters")
    # Change rotational speed of lunar module
    if lunar_module.clockwise_thruster:
        draw_burning_fuel("clockwise")
        lunar_module.rotation -= rotation_step
    if lunar_module.anticlockwise_thruster:
        draw_burning_fuel("anticlockwise")
        lunar_module.rotation += rotation_step
    # Rotate lunar module
    lunar_module.left(lunar_module.rotation)

    # Apply effect of gravity
    apply_force("gravity")

    # Translate lunar module
    x = lunar_module.travel_speed * math.cos(
        math.radians(lunar_module.travel_direction)
    )
    y = lunar_module.travel_speed * math.sin(
        math.radians(lunar_module.travel_direction)
    )
    lunar_module.setx(lunar_module.xcor() + x)
    lunar_module.sety(lunar_module.ycor() + y)

    # Refresh image of lunar module
    draw_lunar_module()

    # Check for successful or unsuccessful landing
    successful_landing = check_landing()
    if successful_landing is not None:
        if successful_landing:
            window.title("Well Done! You've landed successfully")
        else:
            window.bgcolor("red")
            window.title("The lunar module crashed")
        break

    time.sleep(0.05)
    window.update()

turtle.done()

The module’s landing position is shifted vertically upwards from the landing pad by a distance equal to branch_size since this position refers to the centre of the lunar module.

The check_landing() function first checks whether the lunar module’s (x, y) position is within the tolerance range. If it is, then there are two possible outcomes:

  • The lunar module’s orientation is within the tolerance range. The position and orientation of the lunar module are set to the correct landing values so that the spaceship “snaps” in place. The function returns True.
  • The lunar module’s orientation is outside the tolerance range. This means the spaceship has crashed on the landing pad. The function returns False.

The function also returns False if the lunar module falls below the lower edge of the window. This case corresponds to the lunar module crashing on the moon’s surface.

If neither of these conditions is met, the function returns None, which means that the lunar module is still in flight.

Your final step is to check for each of these scenarios in the while loop and end the game with a success or failure warning.

Here’s the output of the final version of this Python lunar landing game:

Final Words

You’ve completed the Python lunar landing game. By using the turtle module, you’ve been able to build the game from first principles, controlling how the Turtle objects move and how they interact with each other.

However, there’s more you can add if you want to upgrade the game. For example, you can ensure that the lunar module doesn’t land with a speed that’s too high, or you can set a limited amount of fuel.

Have a go at adding more features to this game!

Further Reading


Get the latest blog updates

No spam promise. You’ll get an email when a new blog post is published


Leave a Reply