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 windowwindow = 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 randomimport turtle# Set up the game windowwindow = 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 parametersn_of_stars = 100# Create stars and moonstars = 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 theTurtle
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()
andsety()
set only one of the coordinates, either x or y.
You’ve now set up the background for this Python lunar landing game:
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 randomimport turtle# Set up the game windowwindow = 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 parametersn_of_stars = 100# Create stars and moonstars = 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!
Become a Member of
The Python Coding Place
Video courses, live cohort-based courses, workshops, weekly videos, members’ forum, and more…
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 randomimport turtle# Set up the game windowwindow = 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 parametersn_of_stars = 100# Lunar module design parametersbranch_size = width / 16n_of_discs = 5disc_colour = "light gray"centre_colour = "gold"landing_gear_colour = "red"# Create stars and moonstars = 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 modulelunar_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 laterdraw_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:
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:
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 randomimport turtle# Set up the game windowwindow = 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 parametersn_of_stars = 100# Lunar module design parametersbranch_size = width / 16n_of_discs = 5disc_colour = "light gray"centre_colour = "gold"landing_gear_colour = "red"# Create stars and moonstars = 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 modulelunar_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 laterprint(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 randomimport timeimport turtle# Set up the game windowwindow = 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 parametersn_of_stars = 100# Lunar module design parametersbranch_size = width / 16n_of_discs = 5disc_colour = "light gray"centre_colour = "gold"landing_gear_colour = "red"# Lunar module movement parametersrotation_step = 0.2# Create stars and moonstars = 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 modulelunar_module = turtle.Turtle()lunar_module.penup()lunar_module.hideturtle()lunar_module.setposition(-width / 3, height / 3)lunar_module.rotation = 0lunar_module.clockwise_thruster = Falselunar_module.anticlockwise_thruster = Falsedef 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 = Truedef turn_on_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Truewindow.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 = Truedef turn_on_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Truedef turn_off_clockwise_thruster(): lunar_module.clockwise_thruster = Falsedef turn_off_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Falsewindow.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 randomimport timeimport turtle# Set up the game windowwindow = 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 parametersn_of_stars = 100# Lunar module design parametersbranch_size = width / 16n_of_discs = 5disc_colour = "light gray"centre_colour = "gold"landing_gear_colour = "red"# Lunar module movement parametersrotation_step = 0.2# Create stars and moonstars = 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 modulelunar_module = turtle.Turtle()lunar_module.penup()lunar_module.hideturtle()lunar_module.setposition(-width / 3, height / 3)lunar_module.rotation = 0lunar_module.clockwise_thruster = Falselunar_module.anticlockwise_thruster = Falsedef 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 fuelburning_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 = Truedef turn_on_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Truedef turn_off_clockwise_thruster(): lunar_module.clockwise_thruster = Falsedef turn_off_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Falsewindow.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 modulelunar_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 = Falselunar_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 modulelunar_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 = Falselunar_module.anticlockwise_thruster = Falselunar_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 mathimport randomimport timeimport turtle# Set up the game windowwindow = 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 parametersn_of_stars = 100# Lunar module design parametersbranch_size = width / 16n_of_discs = 5disc_colour = "light gray"centre_colour = "gold"landing_gear_colour = "red"# Lunar module movement parametersrotation_step = 0.2# Create stars and moonstars = 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 modulelunar_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 = Falselunar_module.anticlockwise_thruster = Falselunar_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 fuelburning_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 = Truedef turn_on_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Truedef turn_off_clockwise_thruster(): lunar_module.clockwise_thruster = Falsedef turn_off_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Falsewindow.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:
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 parametersn_of_stars = 100# Lunar module design parametersbranch_size = width / 16n_of_discs = 5disc_colour = "light gray"centre_colour = "gold"landing_gear_colour = "red"# Lunar module movement parametersrotation_step = 0.2speed_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 moduledef 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 parametersn_of_stars = 100# Lunar module design parametersbranch_size = width / 16n_of_discs = 5disc_colour = "light gray"centre_colour = "gold"landing_gear_colour = "red"# Lunar module movement parametersrotation_step = 0.2speed_step = 0.1gravity = 0.03# ...# Applying forces to translate the lunar moduledef 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 mathimport randomimport timeimport turtle# Set up the game windowwindow = 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 parametersn_of_stars = 100# Lunar module design parametersbranch_size = width / 16n_of_discs = 5disc_colour = "light gray"centre_colour = "gold"landing_gear_colour = "red"# Lunar module movement parametersrotation_step = 0.2speed_step = 0.1# Landing parameterslanding_pad_position = 0, -height / 2.1module_landing_position = ( landing_pad_position[0], landing_pad_position[1] + branch_size,)landing_pos_tolerance_x = 20landing_pos_tolerance_y = 5landing_orientation = 270 # vertically downwardslanding_orientation_tolerance = 15gravity = 0.03# Create stars and moonstars = 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 padlanding_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 modulelunar_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 = Falselunar_module.anticlockwise_thruster = Falselunar_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 fuelburning_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 = Truedef turn_on_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Truedef turn_off_clockwise_thruster(): lunar_module.clockwise_thruster = Falsedef turn_off_anticlockwise_thruster(): lunar_module.anticlockwise_thruster = Falsewindow.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 moduledef 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 landingdef 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 yetwhile 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!
Subscribe to
The Python Coding Stack
Regular articles for the intermediate Python programmer or a beginner who wants to “read ahead”
Further Reading
- If you enjoyed using the
turtle
module to create this Python lunar landing game, you can also look at the article Practise Using Lists, Tuples, Dictionaries, and Sets inn Python With the Chaotic Balls Animation - If you want to learn the basics of defining functions, you can read Chapter 3 about defining Python functions and Chapter 6 which delves deeper into functions
Become a Member of
The Python Coding Place
Video courses, live cohort-based courses, workshops, weekly videos, members’ forum, and more…