Choose Your Fighter • Let's Play (#1 in Inheritance vs Composition Pair)
The first in the inheritance vs composition pair of articles • Step-by-step tutorial to set up the inheritance version of this `turtle` game
Everyone does this. You learn a new topic, and then you feel the urge to use it in every program you write. Inheritance in object-oriented programming is one of these topics. It's useful, but it's not always the solution.
I'm splitting today's article into two. But they're not Part 1 and Part 2 of the same post. Both articles can stand alone. The first article—this one—is a detailed step-by-step tutorial guiding you through writing the code for a simple game–the game is simple, not the code. You'll write the version that uses inheritance. If you're not interested in a step-by-step tutorial but are curious about the inheritance vs composition comparison, you can go straight to the second article.
First article of the year, so let's start with a fun project: a simple game using the turtle
module—I love using the turtle
module. But in this case, this gives us the perfect opportunity to explore some OOP concepts.
You'll write this game:
The enemy turtles enter the screen at random time intervals, travelling at different speeds and in different directions. They also change direction every so often. You control the laser shooter in the middle, and—you guessed it—your mission is to shoot down as many enemy turtles as possible. You get more points when you shoot down the faster enemies. But beware! You have limited ammunition.
Over the holidays, I’ve been busy publishing Breaking the Rules—a book about borrowing storytelling techniques for technical writing. I also launched a community for technical writers. If you’re a budding technical writer, or maybe you’re an experienced one, and you’re interested in a different take on technical writing, get the book or join the community.
Getting Started • Creating the Screen and Player
Don't worry if you haven't used the turtle
module. I'll explain whatever you need to know as you progress through this tutorial. But I'll also try to be succinct, as there are many steps in this tutorial.
First, create the screen. Choose the size and the background colour:

You should see a window with a grey background when you run this code. You need the final line to keep the window open. Otherwise, the program ends and closes the window immediately.
Next, create the laser shooter. This is the game element the player can control:
You'll use the Turtle
object extensively in this tutorial. In the code so far, you use methods to set the shape of this object on the screen, stretch it to make the arrow more pointy, and change its colour. You'll see the laser shooter in the middle of the screen, pointing towards the right.
Control the Laser Shooter • Binding Functions to Keys
It's time to add some movement. In this step, you'll also start working on the game loop, which is a while
loop that deals with everything that happens in each frame of the game.
Let's look at the update to the code first, and then we'll discuss the changes below. The changes from the previous version are highlighted. Note that you can now remove the call to turtle.done()
from the end of the code:
Here are the steps you take when making these changes:
You define a
player_rotation_step
variable to set the amount (in degrees) you want the shooter to turn in each step.You add a data attribute
.rotation
toplayer
to store the current state of the shooter's rotation speed. Note that.rotation
is not one of the attributes for aTurtle
object. You add it yourself. More on this technique below. Initially, you set its value to0
since the laser shooter is stationary at the start of the game.You add the main game loop, which is a
while
loop. You use theTurtle
object's.left()
method to turn the laser shooter. Ifplayer.rotation
is0
, as it is at the start of the game, then the shooter doesn't turn.You define three functions. Two of them,
turn_left()
andturn_right()
, set the newplayer.rotation
attribute to theplayer_rotation_step
value you set earlier. One of them has a negative sign to represent the opposite direction. The third function,stop_turning()
, resetsplayer.rotation
to0
.You bind these functions to the key presses and key releases. This step enables you to start turning left or right when you press the arrow keys. The laser shooter stops turning when you release the arrow keys. You also need to add
screen.listen()
to ensure the program is aware you'll be interacting with it using keys on the keyboard.
You customise player
, which is a Turtle
object, by adding a new data attribute, .rotation
. Python allows you to add new attributes to an object on the fly. Probably it's neater to define a new class when you customise an object's behaviour. But you'll define two new classes later in this tutorial, and I'd like to focus on them. Also, there's only one player
object in the code. So, I chose to use this hack in this tutorial for the little customisation that player
needs.
You should be able to turn the shooter using the arrow keys when you run the code. Note that on some setups, you need to click on the animation window to bring it into focus before you can use the arrow keys.
You can modify the value of player_rotation_step
to control how slow or fast you want the shooter to turn when you press the arrow keys.
OK, OK, let's step it up a bit now.
Adding the Turtle Enemies • Inheritance
Your planet is being attacked by alien enemies that look like turtles. Whatever! You don't care about the back story. You need enemies. Lots of them. They're all similar but not identical. This is what classes are for.
You can create an Enemy
class to create as many enemies as you need. But you want your enemies to be able to move around, turn, look like turtles… You're using the turtle
module for a reason–it provides a relatively easy way to do all these things.
So, you can define the Enemy
class to inherit from Turtle
. This makes an Enemy
object first and foremost a Turtle
object, but then you can customise it further.
Spoiler alert: you'll look at an alternative approach in this article's twin post.
You can define the class in a new file, game_entities.py
:
You add turtle.Turtle
in parentheses after the class name when you define the class to indicate that your class inherits from turtle.Turtle
. You also call the super class's .__init__()
method using super().__init__()
. This step initialises the Enemy
object as a Turtle
object first before you add further initialisation steps in Enemy.__init__()
.
An Enemy
object is also a Turtle
object. Therefore, you can use the Turtle
object's attributes, such as .shape()
, .penup()
, .color()
, and .left()
. The only method you haven't already seen is .penup()
, which stops the turtle from drawing a line on the screen wherever it goes!
Each time you create an instance of Enemy
, a turtle-shaped enemy appears in the middle of the screen—the default position for Turtle
objects. This enemy has a random RGB colour and turns to face in a random direction.
Back in the main program, turtle_invasion.py
, you can add a list to store all the enemies and create lots of enemies in the game loop:
You can run this code now. There are a few issues with this version. The first problem is that this code keeps adding new enemies all the time. You'll fix this next.
Spawning Enemies • Moving Enemies
Let's stay in turtle_invasion.py
. You could set a constant time interval to spawn new enemies. But it's best to randomise the time when a new enemy appears. Let's look at the changes you need to make in the code first and then explain them later:
Here are the changes you make:
You set a range of time intervals using a tuple,
spawn_interval_range
.You start a spawn timer just before the start of the game loop and set the time delay to spawn the first enemy to
0
. This step will create an enemy in the first frame of the game. You'll change this time delay in the loop once you create the first enemy.You move the line that creates an
Enemy
within anif
block. You also place the line that adds this enemy to the list of enemies in this block. Theif
statement checks whether the time elapsed since you started the spawn timer is greater than the required delay.Finally, choose a new delay value that determines when the code spawns the next enemy. You also reset the spawn timer.
You use random.uniform()
to set a new delay. This function from the random
module picks a random float from the range set by the two arguments you pass to the function. The unpacking operator *
unpacks the tuple spawn_interval_range
into two separate arguments.
You can run the code to see the output. There is now a delay between the creation of an enemy and the next one. But this is a good time to take more control of how the program draws the animation. Note how each time a new enemy is spawned, you have to wait for it to turn to its final orientation. The turtle
module displays every step when turning and moving Turtle
objects. This behaviour will create lag in the game. However, you can control when the display is updated by using a pair of turtle
methods: .tracer()
and .update()
:
Make sure the screen.update()
line only has one level of indentation. It's part of the while
loop, but it's not part of the if
block preceding it in your code.
You'll want to ensure each enemy spawns outside the screen rather than in the middle. Although this would be a good time to deal with this step, it's best to move the enemies first since you can see them now.
First, add an .enemy_speed
data attribute to the Enemy
class in game_entities.py
. You also define a class attribute, .SPEED_RANGE
, which sets the minimum and maximum speeds for the enemies. If you prefer to give control of this range to the user of the class, you can expose it through a method, for example. But this tutorial is already lengthy, so I'll set the range as a class attribute here.
Finally, add a .move()
method:
Next, call this method in the game loop in turtle_invasion.py
. Each iteration of the while
loop represents a frame of the game. Therefore, the .move()
method is responsible for moving the enemy the distance needed in each frame:
And each enemy now moves at its own speed. Try it out.
You can set each enemy to start off outside the screen when they're spawned. First, you can define a new method in the Enemy
class:
This method needs the dimensions of the screen to place the enemies at the edge of the screen. And now, you can call this method when you create a new enemy:
Now, you have enemies that spawn every few seconds at the edge of the screen and move at different speeds.
Note that if your enemies are moving too fast or too slow, you can change the values in the tuple .SPEED_RANGE
, which is the class attribute in the Enemy
class. The numbers in this tuple represent the minimum and maximum speeds.
Changing the Enemy's Direction of Travel • And Adding a Timer for Each Enemy
Let's make it a bit more interesting and get the enemies to change direction every few seconds. Each enemy will have its own schedule. Some will change direction more frequently than others.
You need to make a few changes to the Enemy
class in game_entities.py
:
Here are the changes:
You define two new class attributes to set the maximum turning angle and the range of time intervals for enemies to change direction.
You add a new data attribute in the class's
.__init__()
method,.turning_period
, which sets a time interval for each enemy after which it changes direction. EachEnemy
object has its own time interval chosen from the interval set by.TURNING_PERIOD_RANGE
.You add a
.start_timer
data attribute and set its initial value to the time the enemy is created.You define a
.change_direction()
method that checks whether the timer has elapsed. If it has, the enemy changes direction by a random angle and you reset the timer.
And finally, call this new method in turtle_invasion.py
:
And now, the enemies are making it harder for you to track:
Clearing Enemies That Go Out of Bounds • Version One
This looks good so far, but beware what you can't see! Add the following line to your while
loop:
Let the code run for a while, and you'll see that the number of items in the list enemies
keeps increasing. The Enemy
objects are still present in the program even when they're off the screen.
First, add a new method in the Enemy
class to check whether the enemy has left the screen:
You also need to update the section that deals with moving the enemies in the main game loop in turtle_invasion.py
:
Note that there's also a change in the for
statement. You loop through enemies.copy()
instead of directly through the list enemies
since you shouldn't loop through a list that you're also changing within the loop. When you remove an item from the list, all the following ones move down a spot since you can't have empty places in a list. As a result, one element is skipped in the loop. Looping through a copy of the list is one way of dealing with the issue.
When you run this code, you'll notice that the number of enemies printed decreases each time an enemy leaves the screen.
All seems well. However, this doesn't free up any memory! The turtle
module keeps an internal list of all the Turtle
objects it creates, which you can access through the function turtle.turtles()
. You can modify the print()
call in the final line:
Even though you remove an enemy from the enemies
list when it leaves the screen, the object is still in the list that you access through turtle.turtles()
. However, if you also remove the enemy from this list, there won't be any more references to the object, and Python clears it from memory:
Note that player
is also a Turtle
object and, therefore, it's included in turtle.turtles()
.
You've covered a lot. This would be a good time for a break if you need one. Next, you'll work on shooting lasers.
Shooting Lasers • Another Class
You have enemies spawning every few seconds, moving around the screen, and leaving the screen. You also have the laser shooter, which you can control with the arrow keys on your keyboard.
But to defeat the invading enemies, you need to shoot them down with lasers. It's time to create another class to deal with all the laser shots:
This class also inherits from turtle.Turtle
. You create a few default values as class attributes. These attributes set the length and thickness of the line to represent the laser on the screen and its speed.
The .__init__()
method initialises the Laser
object by calling Turtle
methods and creating a .laser_speed
instance method.
Note that the .__init__()
method includes a parameter that you name player
. Therefore, you need to pass the player—a Turtle
object—when you create an instance of Laser
. This object is used to set the position and heading of the laser to match the player.
Although the class has a default laser speed value, you also create a method to allow the user of the class to set the laser speed:
What else do you need the laser to do? You need it to move. When you dealt with the enemy, you used the shape of the Turtle
object to show the enemy on the screen. You'll use a different approach to show the laser. You can draw a line each time you need to display a laser shot. Define two new methods to draw and move the laser:
You could include all the code within .move()
, but it's neater to define the method ._draw()
that's called by .move()
to separate the logic. The leading underscore in ._draw()
is a convention that indicates this attribute is non-public, which means it's only intended for use within the class and is not part of the public interface of the class. Anyone using the Laser
class should never need to call ._draw()
directly.
The first statement in ._draw()
clears the previous line, the one from the previous frame, before redrawing a new line. Therefore, the laser will appear to move as the code first draws a line, then it moves the object forward, clears the old line, and draws a new one in the new, shifted position.
Now you're all set to create lasers in turtle_invasion.py
. You'll create a new laser each time you hit the spacebar.
Here are the updates to the code:
And here are the steps:
You set a
laser_speed
value that determines the speed of the lasers in the game.You create an empty
lasers
list to store all the active lasers in the game.You define a
shoot_laser()
function that creates a newLaser
instance, sets its speed, and adds it to the listlasers
. You also bind this function to the spacebar key.Finally, you loop through all the
Laser
objects in thewhile
loop to move them.
Now, you can shoot lasers when you press the spacebar:
Removing Out-of-Bounds Lasers • No Repetition
You saw this with the enemies already. So you know what to expect. The lasers that leave the screen are still part of the program that's running. They use up memory, and the loop wastes time moving them forward even though you can't see them.
And you know how to fix this. Recall that you added an ._is_out_of_bounds()
method to Enemy
to check whether the enemy left the screen. You can simply copy and paste this method to the Laser
class, too. You need the exact same logic in Laser
.
But copy-pasting is rarely a good idea. Remember the DRY principle—Don't Repeat Yourself?
Since you're using inheritance in this tutorial, let's see whether you can rely on inheritance to solve this issue. Both Enemy
and Laser
inherit from Turtle
. But they both also need the .is_out_of_bounds()
method, which the parent Turtle
class doesn't have.
One option is to create an intermediate step. Create a class that inherits from Turtle
and add the .is_out_of_bounds()
method to it. Then, Enemy
and Laser
can inherit from this new class. This is a perfectly valid option.
However, I'll go down a different route in this tutorial and create a class that only has the ._is_out_of_bounds()
method and nothing else. Let's call this new class OutOfBoundsMixin
. A mixin is a class intended only to provide methods to other classes through inheritance and not to stand on its own.
Enemy
and Laser
can then inherit from both Turtle
and OutOfBoundsMixin
. Multiple inheritance can be tricky and should only be used with classes that have very limited and specific uses, such as the OutOfBoundsMixin
.
Let's define this new mixin class and then make some changes to both Enemy
and Laser
:
You remove the .is_out_of_bounds()
method from the Enemy
class, and you place it in the new OutOfBoundsMixin
class. This class only contains this method. Note that this mixin class doesn't know about the Turtle
methods .xcor()
and .ycor()
. Your IDE may warn you of this. But that's OK as long as you only plan to use this mixin class with classes that also inherit from Turtle
.
Enemy
and Laser
both inherit from both Turtle
and OutOfBoundsMixin
. Therefore, both classes have the .is_out_of_bounds()
method, which they inherit from the mixin class.
You can now use this method to check whether a laser has left the screen in turtle_invasion.py
. When it does, you should clear the drawing and remove the laser from both lists to which it belongs. Sounds familiar? These steps are similar to the lines you use when an enemy leaves the screen. Copy-paste them?
No, let's create a function instead:
The new function, cleanup_entity()
, deals with either enemies or lasers. Note that you need .hideturtle()
to remove the enemy drawing since you're showing the turtle directly. However, you need .clear()
for the laser to clear the line drawn. The function cleanup_entity()
includes both calls, so it can apply in all cases.
You also add len(lasers)
to the final print()
call to confirm that enemies and lasers are both cleared when they leave the screen.
Run this code, shoot a few lasers, and check the values printed out showing the number of enemies, lasers, and the total number of turtles in the program.
And now that this final print()
call has done its job, you can delete the line.
Destroying the Enemies • Scoring Points
You're almost there. You have aliens flying around, and you can shoot lasers. But the lasers fly right through the alien turtles. You can now work on detecting collisions. Start with a new .has_collided_with()
method in the Laser
class. You also need to set a value to determine how close the laser should be to the alien to register the hit:
You rely on the .distance()
method in the Turtle
module to determine whether the laser is close enough to the object passed to .has_collided_with()
. The argument passed to entity
should be a Turtle
object or an object derived from the Turtle
class, such as Enemy
.
Back in turtle_invasion.py
, you can check whether a laser has hit any of the alien enemies on the screen. If there is a collision, you can remove the alien and the laser from the screen. This is a good time to deal with a score, too:
For every laser you iterate through in the for
loop, you check whether it has hit any of the enemies. You use the .has_collided_with()
method you defined for Laser
objects and the cleanup_entity()
function defined in turtle_invasion.py
. This function did indeed save you from repeating the same code several times!
You also initialise the score
variable and increment its value when you hit an enemy. To make the game more interesting, rather than scoring one point for each enemy, you increment the score by the speed of the enemy you hit. Faster enemies are harder to hit, so you deserve more points when you hit them!
Here's what the game looks like so far:
There's only one final step. You're almost done.
Limited Ammunition • Ending the Game
Let's say that the laser shooter only has a limited number of shots. This version of the game ends when you run out of ammunition. You may want to make your version more complex using different rules once you finish this tutorial.
The final steps are not too challenging compared to the rest of the tutorial. Here are these changes implemented in turtle_invasion.py
:
And here's a summary of the changes:
You define a
total_ammunition
variable to set the maximum number of shots allowed.You create a new instance attribute for
player
named.ammunition
to keep track of how much ammunition is remaining during the game.You decrease the player's ammunition each time the player shoots. You add this to the
shoot_laser()
function. You also remove this function's binding from the spacebar when the player runs out of ammunition to ensure the player can't shoot anymore.You change the condition in the
while
statement so that the game ends when the ammunition value reaches0
and the last laser leaves the screen.Finally, you update the display in the title bar to show the score and the amount of ammunition left and add
turtle.done()
again at the end, after thewhile
loop, to ensure the window stays open once the game ends.
And now it's your task to defend your planet from the turtle invasion. The full version of the code in both scripts is available below after the conclusion.
Final Words • Prelude to the Second Article
You made it! This was a long article, but step-by-step tutorials for non-trivial projects tend to be a bit long.
As I mentioned at the start, this article can stand alone as a tutorial for writing a fun game. You can carry on and customise the game further.
But it's also part of a two-article collection that focuses on comparing inheritance with composition. Good news: the second article is shorter than this one. It's not a step-by-step tutorial. Instead, it uses the code you wrote in this tutorial and proposes an alternative version that doesn't use inheritance. See you in the second article?
Code in this article uses Python 3.13
The code images used in this article are created using Snappify. [Affiliate link]
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
And you can find out more about me at stephengruppetta.com
Further reading related to this article’s topic:
"You Have Your Mother's Eyes" • Inheritance in Python Classes
Also part of the video course A Magical Tour Through Object-Oriented Programming in Python • Hogwarts School of Codecraft and Algorithmancy
Python Classes: The Power of Object-Oriented Programming – Real Python
Final Version of the Code
turtle_invasion.py
# turtle_invasion.py
import random
import time
import turtle
from game_entities import Enemy, Laser
WIDTH = 600
HEIGHT = 600
player_rotation_step = 3
spawn_interval_range = 1, 3
laser_speed = 15
total_ammunition = 100
# Initialise variables
enemies = []
lasers = []
score = 0
# Create Screen
screen = turtle.Screen()
screen.tracer(0)
screen.setup(WIDTH, HEIGHT)
screen.bgcolor(0.2, 0.2, 0.2)
screen.listen()
# Create player
player = turtle.Turtle()
player.shape("triangle")
player.shapesize(stretch_len=1.5)
player.color("purple")
player.rotation = 0
player.ammunition = total_ammunition
# Control player
def turn_left():
player.rotation = player_rotation_step
def turn_right():
player.rotation = -player_rotation_step
def stop_turning():
player.rotation = 0
screen.onkeypress(turn_left, "Left")
screen.onkeyrelease(stop_turning, "Left")
screen.onkeypress(turn_right, "Right")
screen.onkeyrelease(stop_turning, "Right")
# Shoot lasers
def shoot_laser():
laser = Laser(player)
laser.set_speed(laser_speed)
lasers.append(laser)
player.ammunition -= 1
if not player.ammunition:
screen.onkeypress(None, "space")
screen.onkeypress(shoot_laser, "space")
# Deal with out-of-bounds objects
def cleanup_entity(entity, entities_list):
entity.hideturtle()
entity.clear()
screen.update()
entities_list.remove(entity)
turtle.turtles().remove(entity)
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while player.ammunition > 0 or lasers:
screen.title(
f"Ammunition: {player.ammunition} "
f"| Score: {int(score):3}"
)
player.left(player.rotation)
# Spawn new enemy if time interval elapsed
if (
time.time() - start_enemy_spawn_timer
> next_enemy_spawn_delay
):
new_enemy = Enemy()
enemies.append(new_enemy)
new_enemy.set_start_position(WIDTH, HEIGHT)
next_enemy_spawn_delay = random.uniform(
*spawn_interval_range
)
start_enemy_spawn_timer = time.time()
# Move enemies
for enemy in enemies.copy():
enemy.move()
enemy.change_direction()
if enemy.is_out_of_bounds(WIDTH, HEIGHT):
cleanup_entity(enemy, enemies)
# Move lasers
for laser in lasers.copy():
laser.move()
if laser.is_out_of_bounds(WIDTH, HEIGHT):
cleanup_entity(laser, lasers)
# check for collisions
for enemy in enemies:
if laser.has_collided_with(enemy):
score += enemy.enemy_speed
cleanup_entity(enemy, enemies)
cleanup_entity(laser, lasers)
screen.update()
# print(len(enemies), len(lasers), len(turtle.turtles()))
turtle.done()
game_entities.py
# game_entities.py
import math
import random
import time
import turtle
class OutOfBoundsMixin:
def is_out_of_bounds(self, screen_width, screen_height):
return (
abs(self.xcor()) > screen_width / 2
or abs(self.ycor()) > screen_height / 2
)
class Enemy(turtle.Turtle, OutOfBoundsMixin):
SPEED_RANGE = 0.1, 3
MAX_TURN_ANGLE = 90
TURNING_PERIOD_RANGE = 0.5, 2
def __init__(self):
super().__init__()
self.shape("turtle")
self.penup()
self.color(
random.random(),
random.random(),
random.random(),
)
self.left(random.randint(1, 359))
self.enemy_speed = random.uniform(*self.SPEED_RANGE)
self.turning_period = random.uniform(
*self.TURNING_PERIOD_RANGE
)
self.start_timer = time.time()
def move(self):
self.forward(self.enemy_speed)
def set_start_position(self, screen_width, screen_height):
self.backward(
math.sqrt(
(screen_width / 2) ** 2
+ (screen_height / 2) ** 2
)
)
if self.ycor() > screen_height / 2:
self.sety(screen_height / 2)
if self.ycor() < -screen_height / 2:
self.sety(-screen_height / 2)
if self.xcor() > screen_width / 2:
self.setx(screen_width / 2)
if self.xcor() < -screen_width / 2:
self.setx(-screen_width / 2)
def change_direction(self):
if (
time.time() - self.start_timer
> self.turning_period
):
self.left(
random.uniform(
-self.MAX_TURN_ANGLE,
self.MAX_TURN_ANGLE,
)
)
self.start_timer = time.time()
class Laser(turtle.Turtle, OutOfBoundsMixin):
LENGTH = 10
THICKNESS = 5
DEFAULT_SPEED = 1
COLLISION_TOLERANCE = 20 # Default diameter for turtles
def __init__(self, player):
super().__init__()
self.hideturtle()
self.penup()
self.color("red")
self.pensize(self.THICKNESS)
self.setposition(player.position())
self.setheading(player.heading())
self.laser_speed = self.DEFAULT_SPEED
def set_speed(self, speed):
self.laser_speed = speed
def _draw(self):
self.clear()
self.pendown()
self.forward(self.LENGTH)
self.forward(-self.LENGTH)
self.penup()
def move(self):
self.forward(self.laser_speed)
self._draw()
def has_collided_with(self, entity):
return (
self.distance(entity) <= self.COLLISION_TOLERANCE
)
Appendix: Code Blocks
Code Block #1
# turtle_invasion.py
import turtle
WIDTH = 600
HEIGHT = 600
# Create Screen
screen = turtle.Screen()
screen.setup(WIDTH, HEIGHT)
screen.bgcolor(0.2, 0.2, 0.2)
turtle.done()
Code Block #2
# turtle_invasion.py
# ...
# Create player
player = turtle.Turtle()
player.shape("triangle")
player.shapesize(stretch_len=1.5)
player.color("purple")
turtle.done()
Code Block #3
# turtle_invasion.py
import turtle
WIDTH = 600
HEIGHT = 600
player_rotation_step = 10
# Create Screen
screen = turtle.Screen()
screen.setup(WIDTH, HEIGHT)
screen.bgcolor(0.2, 0.2, 0.2)
screen.listen()
# Create player
player = turtle.Turtle()
player.shape("triangle")
player.shapesize(stretch_len=1.5)
player.color("purple")
player.rotation = 0
# Control player
def turn_left():
player.rotation = player_rotation_step
def turn_right():
player.rotation = -player_rotation_step
def stop_turning():
player.rotation = 0
screen.onkeypress(turn_left, "Left")
screen.onkeyrelease(stop_turning, "Left")
screen.onkeypress(turn_right, "Right")
screen.onkeyrelease(stop_turning, "Right")
# Main game loop
while True:
player.left(player.rotation)
Code Block #4
# game_entities.py
import random
import turtle
class Enemy(turtle.Turtle):
def __init__(self):
super().__init__()
self.shape("turtle")
self.penup()
self.color(
random.random(),
random.random(),
random.random(),
)
self.left(random.randint(1, 359))
Code Block #5
# turtle_invasion.py
import turtle
from game_entities import Enemy
WIDTH = 600
HEIGHT = 600
player_rotation_step = 10
# Initialise variables
enemies = []
# Create Screen
# ...
# Main game loop
while True:
player.left(player.rotation)
new_enemy = Enemy()
enemies.append(new_enemy)
Code Block #6
# turtle_invasion.py
import random
import time
import turtle
from game_entities import Enemy
WIDTH = 600
HEIGHT = 600
player_rotation_step = 10
spawn_interval_range = 1, 3
# Initialise variables
# ...
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while True:
player.left(player.rotation)
# Spawn new enemy if time interval elapsed
if (
time.time() - start_enemy_spawn_timer
> next_enemy_spawn_delay
):
new_enemy = Enemy()
enemies.append(new_enemy)
next_enemy_spawn_delay = random.uniform(
*spawn_interval_range
)
start_enemy_spawn_timer = time.time()
Code Block #7
# turtle_invasion.py
# ...
# Create Screen
screen = turtle.Screen()
screen.tracer(0)
screen.setup(WIDTH, HEIGHT)
screen.bgcolor(0.2, 0.2, 0.2)
screen.listen()
# ...
while True:
# ...
screen.update()
Code Block #8
# game_entities.py
import random
import turtle
class Enemy(turtle.Turtle):
SPEED_RANGE = 0.5, 10
def __init__(self):
# ...
self.enemy_speed = random.uniform(*self.SPEED_RANGE)
def move(self):
self.forward(self.enemy_speed)
Code Block #9
# turtle_invasion.py
# ...
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while True:
# ...
# Move enemies
for enemy in enemies:
enemy.move()
screen.update()
Code Block #10
# game_entities.py
import math
import random
import turtle
class Enemy(turtle.Turtle):
# ...
def set_start_position(self, screen_width, screen_height):
self.backward(
math.sqrt(
(screen_width / 2) ** 2
+ (screen_height / 2) ** 2
)
)
if self.ycor() > screen_height / 2:
self.sety(screen_height / 2)
if self.ycor() < -screen_height / 2:
self.sety(-screen_height / 2)
if self.xcor() > screen_width / 2:
self.setx(screen_width / 2)
if self.xcor() < -screen_width / 2:
self.setx(-screen_width / 2)
Code Block #11
# turtle_invasion.py
# ...
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while True:
player.left(player.rotation)
# Spawn new enemy if time interval elapsed
if (
time.time() - start_enemy_spawn_timer
> next_enemy_spawn_delay
):
new_enemy = Enemy()
enemies.append(new_enemy)
new_enemy.set_start_position(WIDTH, HEIGHT)
next_enemy_spawn_delay = random.uniform(
*spawn_interval_range
)
start_enemy_spawn_timer = time.time()
# ...
Code Block #12
# game_entities.py
import math
import random
import time
import turtle
class Enemy(turtle.Turtle):
SPEED_RANGE = 0.5, 10
MAX_TURN_ANGLE = 90
TURNING_PERIOD_RANGE = 0.5, 2
def __init__(self):
# ...
self.turning_period = random.uniform(
*self.TURNING_PERIOD_RANGE
)
self.start_timer = time.time()
# ...
def change_direction(self):
if (
time.time() - self.start_timer
> self.turning_period
):
self.left(
random.uniform(
-self.MAX_TURN_ANGLE,
self.MAX_TURN_ANGLE,
)
)
self.start_timer = time.time()
Code Block #13
# turtle_invasion.py
# ...
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while True:
#
# Move enemies
for enemy in enemies:
enemy.move()
enemy.change_direction()
screen.update()
Code Block #14
# turtle_invasion.py
# ...
while True:
# ...
print(len(enemies))
Code Block #15
# game_entities.py
# ...
class Enemy(turtle.Turtle):
# ...
def is_out_of_bounds(self, screen_width, screen_height):
return (
abs(self.xcor()) > screen_width / 2
or abs(self.ycor()) > screen_height / 2
)
Code Block #16
# turtle_invasion.py
# ...
while True:
# ...
# Move enemies
for enemy in enemies.copy():
enemy.move()
enemy.change_direction()
if enemy.is_out_of_bounds(WIDTH, HEIGHT):
enemy.hideturtle()
screen.update()
enemies.remove(enemy)
screen.update()
print(len(enemies))
Code Block #17
# turtle_invasion.py
# ...
while True:
# ...
print(len(enemies), len(turtle.turtles()))
Code Block #18
# turtle_invasion.py
# ...
while True:
# ...
# Move enemies
for enemy in enemies.copy():
enemy.move()
enemy.change_direction()
if enemy.is_out_of_bounds(WIDTH, HEIGHT):
enemy.hideturtle()
screen.update()
enemies.remove(enemy)
turtle.turtles().remove(enemy)
screen.update()
print(len(enemies), len(turtle.turtles()))
Code Block #19
# game_entities.py
# ...
class Laser(turtle.Turtle):
LENGTH = 10
THICKNESS = 5
DEFAULT_SPEED = 1
def __init__(self, player):
super().__init__()
self.hideturtle()
self.penup()
self.color("red")
self.pensize(self.THICKNESS)
self.setposition(player.position())
self.setheading(player.heading())
self.laser_speed = self.DEFAULT_SPEED
Code Block #20
# game_entities.py
# ...
class Laser(turtle.Turtle):
# ...
def set_speed(self, speed):
self.laser_speed = speed
Code Block #21
# game_entities.py
# ...
class Laser(turtle.Turtle):
# ...
def _draw(self):
self.clear()
self.pendown()
self.forward(self.LENGTH)
self.forward(-self.LENGTH)
self.penup()
def move(self):
self.forward(self.laser_speed)
self._draw()
Code Block #22
# turtle_invasion.py
import random
import time
import turtle
from game_entities import Enemy, Laser
WIDTH = 600
HEIGHT = 600
player_rotation_step = 10
spawn_interval_range = 1, 3
laser_speed = 15
# Initialise variables
enemies = []
lasers = []
# Create Screen
# ...
# Create player
# ...
# Control player
# ...
# Shoot lasers
def shoot_laser():
laser = Laser(player)
laser.set_speed(laser_speed)
lasers.append(laser)
screen.onkeypress(shoot_laser, "space")
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while True:
# ...
# Move lasers
for laser in lasers.copy():
laser.move()
screen.update()
Code Block #23
# game_entities.py
import math
import random
import time
import turtle
class OutOfBoundsMixin:
def is_out_of_bounds(self, screen_width, screen_height):
return (
abs(self.xcor()) > screen_width / 2
or abs(self.ycor()) > screen_height / 2
)
class Enemy(turtle.Turtle, OutOfBoundsMixin):
# ...
# REMOVE def is_out_of_bounds() method
class Laser(turtle.Turtle, OutOfBoundsMixin):
# ...
Code Block #24
# turtle_invasion.py
# ...
# Deal with out-of-bounds objects
def cleanup_entity(entity, entities_list):
entity.hideturtle()
entity.clear()
screen.update()
entities_list.remove(entity)
turtle.turtles().remove(entity)
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while True:
player.left(player.rotation)
# Spawn new enemy if time interval elapsed
# ...
# Move enemies
for enemy in enemies.copy():
enemy.move()
enemy.change_direction()
if enemy.is_out_of_bounds(WIDTH, HEIGHT):
cleanup_entity(enemy, enemies)
# Move lasers
for laser in lasers.copy():
laser.move()
if laser.is_out_of_bounds(WIDTH, HEIGHT):
cleanup_entity(laser, lasers)
screen.update()
print(len(enemies), len(lasers), len(turtle.turtles()))
Code Block #25
# game_entities.py
# ...
class OutOfBoundsMixin:
# ...
class Enemy(turtle.Turtle, OutOfBoundsMixin):
# ...
class Laser(turtle.Turtle, OutOfBoundsMixin):
LENGTH = 10
THICKNESS = 5
DEFAULT_SPEED = 1
COLLISION_TOLERANCE = 20 # Default diameter for turtles
# ...
def has_collided_with(self, entity):
return (
self.distance(entity) <= self.COLLISION_TOLERANCE
)
Code Block #26
# turtle_invasion.py
# ...
# Initialise variables
enemies = []
lasers = []
score = 0
# ...
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while True:
screen.title(f"Score: {int(score):3}")
# ...
# Move lasers
for laser in lasers.copy():
laser.move()
if laser.is_out_of_bounds(WIDTH, HEIGHT):
cleanup_entity(laser, lasers)
# check for collisions
for enemy in enemies:
if laser.has_collided_with(enemy):
score += enemy.enemy_speed
cleanup_entity(enemy, enemies)
cleanup_entity(laser, lasers)
screen.update()
Code Block #27
# turtle_invasion.py
import random
import time
import turtle
from game_entities import Enemy, Laser
WIDTH = 600
HEIGHT = 600
player_rotation_step = 10
spawn_interval_range = 1, 3
laser_speed = 15
total_ammunition = 100
# ...
# Create player
player = turtle.Turtle()
player.shape("triangle")
player.shapesize(stretch_len=1.5)
player.color("purple")
player.rotation = 0
player.ammunition = total_ammunition
# ...
# Shoot lasers
def shoot_laser():
laser = Laser(player)
laser.set_speed(laser_speed)
lasers.append(laser)
player.ammunition -= 1
if not player.ammunition:
screen.onkeypress(None, "space")
# ...
# Main game loop
start_enemy_spawn_timer = time.time()
next_enemy_spawn_delay = 0
while player.ammunition > 0 or lasers:
screen.title(
f"Ammunition: {player.ammunition} "
f"| Score: {int(score):3}"
)
player.left(player.rotation)
# ...
screen.update()
turtle.done()
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
And you can find out more about me at stephengruppetta.com