Game Tutorial: Space Invaders
I put together a little Space Invaders game and thought I'd write a tutorial on how it works. (You'll want to run it in the REPL Run environment, since Space Invaders requires a fair amount of screen real estate to play.) Feel free to fork the REPL and add to it!
The game is broken up into six main files:
game.py (which handles the game logic),
fleet.py (which handle drawing individual invaders and a whole fleet of invaders, respectively),
player.py (which manages moving the player on the screen),
laser.py (so the player can fire at the invading fleet), and
main.py (which ties everything together and creates a new game). Let's look at each one in turn.
game.py file houses our
Game class, which manages the behavior and data needed to run a Space Invaders game. We'll go through each method one at a time, but here's the file in its entirety if you're curious:
All right! Let's start with our
As you can see, when we initialize a new game, we save a reference to
stdscr (a window object representing the entire screen). This is part of the
curses Python library, which you can read more about here. We also call
_initialize_colors to set up our terminal colors (more on this soon), initialize our
last_tick to the current time, and save references to our window dimensions (
self.window = self.stdscr.getmaxyx()), fleet of invaders (
self.fleet = Fleet(stdscr, self.window)), and the human player (
self.player = Player(stdscr, self.window)). Note that our fleet of invaders and player each get passed references to the overall screen in the form of
self.window; we'll see why in a little bit.
run method just creates an infinite loop that starts our game a-tickin':
tick, all we do at the moment is delegate to our
update method. (We could imagine including other functionality here as well; even though all we do is
update, I like wrapping that behavior in
tick, since it creates a common API for all our game components.)
update, it handles... well, updating our game!
First, we create a
new_tick equal to ten milliseconds (this is how long we wait between updates—that is, the amount of time that passes between each refresh of the game screen). We update our
self.last_tick by adding the
new_tick amount, then call
tick on our fleet and player so they can update, too (passing in the
self.last_tick in order to keep our game clock synchronized). We check to see if there are any collisions (that is, if any of the lasers fired by the player have hit any of the invaders), and finally check
self.is_over() to see if our game has ended, providing appropriate messages depending on whether the player has won or lost (more on this soon).
Let's see how we detect collisions between lasers and invaders:
We loop over all the lasers and invaders, and if we find a collision (more on this soon), we do three things:
- We increment the invader's color (this has the effect of making the invader flicker when hit, since it will cycle through all the colors from red to black); we'll see more about how colors work with the
curseslibrary when we get to our
- If the invader's color is
8(this happens to be the color black), we decrement the number of
remaining_invadersby one (treating the invader as destroyed).
- If the invader's color ever exceeds
8, we just set it back to
8(to ensure the blocks that make up the invader stay black, matching the game background).
The three methods we use to check whether the game has ended are
lost(); each is pretty short, so let's look at them all at once.
To check if a player has
won(), we just check whether there are no remaining invaders. A player has
lost() when the fleet's
y value (its height above the bottom of the screen) is greater than or equal to the player's (meaning the fleet has landed/invaded, since it's gotten down to where the player is on the screen). The game
is_over() when the player either wins or loses.
end() the game like so, by writing an appropriate message (like "You won!" or "Oh no, you lost!") and exiting the program using Python's
Okay! Let's get back to collision detection. We know there's a collision if any of part of a laser overlaps with any part of an invader. This can be a little tricky to compute, since we have to take the
y coordinates of each block into account, as well as those blocks' heights and widths. One way to do it is to say that there's no collision if we shoot wide (too far left or right), high, or low, and that otherwise, we must have a collision. So! That's what we do in
_collision_found(): we check to see if we've missed by going too far left, right, high, or low, and if we haven't missed in those directions, we must have made a hit:
Finally, we finish up our
Game class with a little utility method that sets all the colors we're going to use (you can read more about setting colors in
curses using the
init_pair() function here:
All right! Let's move on to our
Invader class, where we'll start to see how to draw objects on the screen using
curses. We'll also start to see a common API emerge among our game components: most of them have an
__init__() method (to set up the object with attributes like location, color, and direction), a
draw() method (to draw the object on the screen), and a
tick() method (to determine how our game objects should change and behave with each game step). (Oftentimes, our
tick() method just delegates to an
update() method, but as mentioned, we wrap that for now in case we want to add extra functionality later.) Again, we'll go through each method one-by-one, but here's the whole class if you just want to dive in:
Okay! As usual, let's start by looking at our
As mentioned, we start off by saving references to our screen and window objects via
self.stdscr = stdscr and
self.window = window. We also set a
width (to help detect how far across the screen our invader extends) and
speed (to control how quickly it moves), as well as a
direction (+1 for left-to-right and -1 for right-to-left). We also set a
self.range (equal to the max width minus one block and the width of our invader) that ensures our invaders don't try to wander off the screen, as well as
y coordinates.(Note that we pass a
position to our constructor to tell the invader where to draw itself on the screen; the
position is an
(x, y) tuple.)
We set our
8 (red and black, respectively), set our
last_tick to the current time, and our
move_threshold to 0.5 (this will help us slow our invaders down, ensuring they only move once every half-second).
Next up is our
__repr__() is a built-in Python function that you can override to control the printed representation of your object. We return a two-dimensional list of characters, using
'O' to represent a red block and
' ' to represent a black (empty) block. If you look closely, you can see it looks like our on-screen invader!
In order to
draw() our invader, we iterate over the characters in our
self.__repr__() two-dimensional list, drawing a red block when we see a
'O' and an empty/black block when we see
_draw_block() method takes an
x (column position),
y (row position), and
color and adds the block to the screen using
stdscr.addstr() method (which you can read more about here):
Now that we know what we need in order to draw our invader, let's take a look at how we get it to move. Every game
tick, we want to make a decision about our invader's position (using its
y coordinates) so we can redraw it in its new position:
The first line of code in this method is a little confusing, but what we're doing is looking at the difference between the current time and our prior tick. If enough time has passed, update our
x value by one (moving a little to the right), adjusting our
x to the screen range minimum (in case we're about to fall off the left side of the screen) or the screen range maximum (in case we're about to fall off the right side of the screen). We update our
x position by multiplying by our speed (how many columns we move per tick) and direction (+1 to go right-to-left, -1 to go left-to-right). Finally, we update our
self.last_tick in preparation for the next game loop.
In order to
update() our screen, we just need to move and redraw:
As mentioned, our
tick() method just wraps
update() for now:
...and that's all we need to set up our
Invader class! Now let's look at what we need to do to organize our invaders into a
Fleet class is pretty simple! We'll walk through its three methods (
y()), but here's the whole thing:
As usual, our
__init__() method starts by saving references to
window, as well as setting a
range (these are actually identical to what we did in our
Invader class, since we only need a single invader's width in order to determine whether we're about to crash into a wall). If you fork this REPL to add new functionality, fix bugs, or refactor the code, it might be a good idea to use the invader's width and range (rather than duplicating that code here)!
Next, we create a list of
self.invaders. In our case, we set up four invaders that are 15 blocks apart (
xs of 5, 20, 35, and 50) and all at the same
y (2). (Again, if you fork this code, it might be a good idea to set these
x values based on the number of invaders we have, rather than hard-code them.) We also set a
5 (we'll use this to determine how far to "drop down" after our fleet has moved all the way across the screen), a
last_tick of the current time, a
move_threshold of 1 (similar to what we did to control the rate of movement for our invaders), a
number_of_invaders equal to the length of our
self.invaders list, and finally,
remaining_invaders equal to
number_of_invaders (we'll decrement this value as invaders are destroyed by the player).
tick() method controls the movement of our overall fleet. Since invaders have a tick method and can control their won left-to-right movement, we simply call
tick() on all the invaders in
self.invaders to move them left-to-right. We use the same "brake" we used for our invaders to prevent them from updating too quickly, and if our invading fleet is about to drive off the screen, we reverse direction, drop down, and update our
last_tick. (The first branch in our
if statement handles left-to-right movement causes us to drop down and reverse direction instead of falling off the right side of the screen; the
elif handles right-to-left movement and preventing us from falling off the left side of the screen.)
Finally, we create a helper function called
y() that just gets the current
y value (row position) of our invading fleet. (All our invaders have the same
y value, so we arbitrarily take the first one in our fleet, since a fleet should include at least one invader):
Again, if you fork this REPL, it might be a good idea to set an
invader.height = 8 so we don't have to sprinkle this "magic number" throughout our code in order to take the invaders' heights into account.
On to the
You know the drill by now! Here's the whole
__init__() method, we do a lot of familiar things: save references to our screen (
stdscr) and window (
window), set a width (
self.width = 6), a window range (so we don't fly off the screen), a speed, a color, and
y coordinates. And just like a
Fleet has a list of invaders, a
Player has a list of lasers to fire! (We'll see how lasers work soon.)
draw() method is pretty straightforward: we erase our old position with
self.stdscr.erase(), then draw a new player (which is just a green rectangle) at the specified
tick method does a few things (and we could delegate some of them to an
update() method if we wanted!): we update each laser in our array of lasers, respond to user input (which we'll cover in just a minute), and redraw the screen to reflect our changes in the terminal.
Unlike our other game object, the
Player has to respond to human input (and the game loop has to be pretty fast in order for the animation to be fast—that's why we set the overall game loop to 10 milliseconds earlier, but we use our "brake" to ensure invaders move more slowly). To accomplish that, we have our
Here, we use
stdscr.getch() method to determine what key the player is pressing, storing that in
instruction. If it's the left arrow key (
if instruction == curses.KEY_LEFT), we move left; if it's the right arrow key (
if instruction == curses.KEY_RIGHT), we move right. We ensure we don't drive off the board by setting
x to its min value (the left side of the screen) if we're about to go below that, and we set
x to its max value (the right edge of the screen) any time we're about to go above that and drive off the right side of the screen. If the user presses the space bar (
if instruction == ord(' ')), we fire a laser!
Next up: the
This is a short one—here's the file in its entirety:
There's not a ton going on here, so while we'll still go through each method, we won't look at each code snippet individually.
As usual, we have
draw() methods. There's nothing you haven't seen before in
draw(), so we'll focus on
tick(), which does two things: it changes our laser color from white to black when it reaches the top of the screen (to simulate our lasers going off the top of the terminal window), and it decrements the laser's
y value (moving it one row up on the screen) for each tick of the game. Since a laser gets its initial
y values from the player, the end result is a little white laser bolt flying across the screen from the player toward the invading fleet!
Now that we have all the pieces of our game in place, we can tie everything together neatly by creating a new
Game instance in
We have only a single function here,
main, that we call at the bottom of our file (using the
wrapper object from
curses in order to automate all the setup and teardown work needed to neatly move from the regular REPL into the game terminal; you can read more about it here). Here's what
- It sets
curses.curs_set(False), which makes the cursor invisible.
- It sets
stdscr.nodelay(True), which makes our
stdscr.getch()call non-blocking (that is, our game will keep ticking while it waits for user input).
- It clears the screen in preparation for a new game using
- Finally, we create and run a new game via
...and that's it!
There are lots of opportunities to make this game better: making it so multiple hits are required to kill an invader, adding multiple rows of invaders, adding scorekeeping functionality, making the invaders move faster over time, and so on. The sky's the limit!
I hope you enjoyed this tutorial, and feel free to fork this REPL to add more functionality.
Just a couple comments. One, you can customize colors in curses using
curses.init_color(), because the repl.it allows custom colors (
curses.can_change_colors() returns true). And... I forgot the other.
I plan to make a "terminal arcade", where you can play games in the terminal. I think this tutorial will definitely help me :) now I just need to become motivated.
“Welcome to We Buy Houses Palm Beach, the Official Cash Home Buyer of Palm Beach, Florida, and surrounding areas.
We provide home sellers with a cash option to purchase their property quickly—with NO Repairs, and NO Fees or Commissions.
Simply go to our website or call us at (561) 232-3350 to get a no-obligation cash home offer sent to you. It’s free and confidential.
Avoiding foreclosure? Facing divorce? Moving? Upside down in your mortgage? Liens? It doesn’t matter whether you live in it, you’re renting it out, it’s vacant, or not even habitable. We help owners who have inherited an unwanted property, own a vacant house, are behind on payments, owe liens, downsized, and can’t sell… even if the house needs repairs that you can’t pay for… and yes, even if the house is fire damaged or has bad rental tenants.
Basically, if you have a property and need to sell it… we’d like to make you a fair cash offer and close on it when you’re ready to sell.
Just contact us today to get started!
nice job! I love all your tutorials