Seminar 17 Space invaders: mixins and duck typing
Today we will program a classic Space Invaders game with a twist. We will be using object-oriented programming but you will learn about mixins and duck typing. Previously, you have learned how to ensure generalization — common behavior for different classes — via inheritance. E.g., when all visual stimuli inherit from the save BaseVisualStim
class, you can be sure that they all have size
, ori
, and pos
properties and that you can draw()
them. However, this approach may not be best suited for cases when the same class must behave as different several classes. One solution is to use multiple inheritance, so that a class is a descendant of several classes and, thus, get behavior from many of them. This is the canonical way of ensuring generalization but you can achieve same means differently.
17.1 Mixins
One way to infuse common behavior into different classes is via a mixin classes. These are classes that only define a single behavior and nothing else. No constructor, typically no attributes. Thus, they are too limited to be used on their own but can be inherited from (mixed into the proper class) enabling that single behavior in the descendant.
Think about all creatures that can fly: insects, birds, bats, astronauts, etc. They do have a common ancestor but that common ancestor did not have the ability to fly. Instead, each line evolved that ability independently and all have evolutionary “cousins” that cannot fly. When programming, you can follow the same pattern of creating a common ancestor for flying and non-flying insects, then implementing ability to fly in the former. Do the same for birds, bats, astronauts, etc. If your implementation must be very detailed and creature-specific, this might be unavoidable. However, if your ability to “fly” is very abstract and, therefore, the same for all creatures in question, you will end up writing the same code for every insect, bird, bar class. An easy copy-paste, of course, but that means you get multiple places with identical implementation, so when you need to change it, you will have to make sure that you do it in all the places (and you gonna miss some, I always do). Alternative? Mixins! You create a class FlightAbility
that implements that common abstract “flying” and, then, you inherit from that class whenever you need a flying someone. Mix a non-flying bird with the FlightAbility
and it can fly! Mix it with an insect: Flying insect! You may also mix in more than one ability. Again, start with a bird that just walks around (chicken?). Add a mixin FlightAbility
and you get a flying bird (pigeon?). Take the walking bird again and mix in SwimAbility
and you get a swimming bird (penguin?). Mix in both and you get a bird that can both fly and swim (swan?)!
You may not need mixins frequently but they are a powerful way of creating an isolated behavior that different classes might need without enforcing strict inheritance structure. PsychoPy is big on mixins. For example, it has ColorMixin
that could be mixed-in to a visual class that needs to work with color, so it implements all the repetitive41 code for translating an arbitrary color representation (string, hexadecimal code, RGB triplet, HSL triplet, single grayscale value, etc.) into the internal RGB color value. It also has TextureMixin
for classes that use textures for drawing objects. In our Space Invaders game, we will use a Mixin class to mix in a “boom sound when exploded” behavior, common to both aliens and the player’s ship.
17.2 Duck typing
Alternatively, you might need your object to behave in a certain way but having a proper class hierarchy is an overkill because this is only one class and you might want some but not all the functionality. The idea is to use “duck typing”, which comes from saying “If it walks like a duck, and it quacks like a duck, then it must be a duck.” In other words, if the only things you care about are walking and quacking, do you need it to be an actual duck42? Will a goose that can walk and quack the same way do? Will a dog that can walk and quack like a duck do? Obviously, the correct answer is “it depends” but in a lot of situations you are interested in a common behavior rather than in a common ancestor.
Duck typing is a popular method in Python. For example, len(object)
is a canonical way to compute length of an object. That object could be a string len("four")
, or a list len([1, 2, 3, 4])
, a tuple len(tuple(1, 4, 2))
, a dictionary len({"A": 1, "B": 5})
, etc. The idea is that as long as a class has a concept of length (number of elements, number of characters, etc.), you should be able compute length via len()
. This is achieved by adding a special methodto the class __len__(self)
that must return an integer. This is called a “hook method”, as it is never called directly but is “hooked” to the len()
function call. Thus, whenever you write len(object)
, it is actually translated into object.__len()__
43.
Another popular duck typing application is an implementation of an iterator: an object that yields one item at a time, so you can do lazy computation44 or loop over them. It must implement two special methods to enable iteration over its items: __iter__(self)
and __next__(self)
. The former is called when iteration starts and it should perform all necessary initializations (e.g., setting internal counter to 0, reshuffling elements, etc.) and must return an iterator object (typically, a reference to itself)45. The __next()__
method is called whenever the next item is needed: if you use iterator in the for
loop, it is called automatically or you can call it yourself via next(iterator_object)
(try it on a range()
iterator). __next()__
must return the next item or raise StopIteration()
exception, signalling that it ran out of items.
Note that iterator does not require an implementation of the __len()__
method! How come? If you have items you can iterate over, doesn’t it make sense to also know their number (length of an object)? Not necessarily! First, if you use an iterator in a for
loop and you just want to iterate over all of the items until you run out of them (which is signalled by the StopIteration()
exception). Thus, their total number and, hence, len()
method is of little interest. Accordingly, why implement a function that you do not need46? Second, what if your iterator is endless (in that case, it is called a generator)? E.g., every time __next__()
is called it returns one random item or a random number. This way, it will never run out of items, so the question of “what is its length” is meaningless (unless you take “infinity” for an answer). This lack of need for __len__()
for iterators is the spirit of duck typing: implement only the methods you need from your duck, ignore the rest.
Below, you will practice duck typing by implementing both __len()__
and two special iterator methods as part of our AlienArmada
class.
17.3 Which one to use?
Now you know three methods to produce common behavior: proper inheritance, mixins, and duck typing. Which one should you use? Depends on what you need. If you have many classes and you can have a well defined inheritance tree, use it. However, if you have some specific behavior that you need in some classes that is hard to fit with the inheritance tree, use mixins. If you have a single class that needs very specific functionality: use duck typing. Any one of them, does not preclude you using others. But be moderate, using too many different paradigms will be confusing for you and a reader.
17.4 Space Invaders
We will program a simply version of the game with aliean armada going gradualy descending. You task is to capture all aliens by firing a teleport beam before one of them rams your ship. Below, you can see my version of the game.
As per usual, the plan is to move slowly in small increments to keep the complexity of changes low. For the assignment, you will need to submit the final product. However, I strongly recommend an iterative submit-get comments-adjust-add another step or two-submit-get comments-… approach. It will be much easier, both for you and for me, if we catch errors or potentially problematic code early47. Here are the steps:
- Start with our usual PsychoPy boilerplate
- Add a
Spaceship
and use mouse to move it along the bottom of the screen - Create an
Alien
and then th wholeAlienArmada
- Add
Laser
, then aLaserGun
, so that the player can fire many laser shots by pressing left mouse key. - Add check for hitting aliens with the laser and teleporting them off the screen.
- Use mixin to add teleport sound.
- Use cool iterators to cycle through lasers and aliens.
- Make alien armada move
- Check for end-of-game.
Before we start, grab images that we will use for the game space-invaders.zip, created specifically for our seminar by Andrej Pastukhov, who said you absolutely must look at this link: Wie die Pixelarts meines Sohnes Andrej entstanden sind48.
17.5 Boilerplate
Create our usual boilerplate code in exercise01.py (you will use the same files for classes but use versioning of the main file, it makes it easier for me to check your code):
- Import what is needed from PsychoPy
- Create a window. I’ve picked 640×480, as our sprites are 32×32 but chose whatever looks good on your screen.
- Create our usual main game loop with
gamover
variable, flipping the window, and checking for escape button press.
Put your boilerplate code into exercise01.py.
17.6 The spaceship
Create a new file for Spaceship
class. It is remarkably boring as it just an image that moves left-right on a horizontal line. Thus, we will create it as a descendant of visual.ImageStim
class. The only method that you need to define right now is the constructor __init()
. It should take win
as the only parameter (apart from self
, of course) and call super().__init__(...)
with win
variable that you’ve got as a parameter, name of the image (raumschiff.png, don’t forget to add the relative path to it), and the pos
ition of the image.
In the main code, create an object and draw it in the main loop. Experiment with the vertical placement. The ship should be somewhere just above the lower edge of the window. For me, vertical position of -0.9 worked quite nicely.
Put your updated code into exercise02.py, plus an Spaceship
class into a separate file.
17.7 The spaceship moves
Now, let us use the mouse to control the horizontal position of the ship and, later on, left button to shoot. In the main loop, use getPos() method to get the position of the mouse and use horizontal component to alter the position of the ship. We want it to remain at the same position vertically, so we want to keep it constant. Note, however, that you cannot assign just a horizontal or vertical components to the position via .pos[0] = ...
. In my PsychoPy 2020.2.10 it does not generate an error but does not change the actual position either. Thus, you need to assign a tuple/list of [new horizontal component, original vertical component]
. Test moving your spaceship around. You can make mouse invisible (see documentation on the parameter) to make it more immersive.
Put your updated code into exercise03.py.
17.8 An alien appears
Before we create an alien armada, let us start by creating a single alien. The class will be very basic, just like Spaceship
. This is why, we will put both Alien
and AlienArmada
into the same file (so, think about its filename). The class itself will very similar to the Spaceship
. The only differences are that it should take (and pass) pos
parameter (unlike the spaceship that always appears at the same location) and that the image should be randomly picked, either alien-1.png or alien-2.png. For testing, place a few alians at arbitrary locations on the screen to check that the image is randomly picked. Do not forget to draw them in the main loop.
Put your updated code into exercise04.py, plus an Alien
class into a separate file.
17.9 An alien armada appears
One alien is neither scary, nor challenging enough. We want more! For this, we will create an AlienArmada
class with a twist. It will be responsible for creating a grid of aliens, moving them around (we will use getter/setter functions for that), allowing outside processes to iterate over them (iterator duck typing), and reporting the number of remaining aliens (length duck typing). As usual, we will do this one step at a time.
Start by creating an AlienArmada
class, below the Alien
class. Our initial version will have constructor, spawn()
, and draw()
methods plus win
, __pos
(a tuple with the position within the window) and aliens
attributes. The latter will be a list with alien objects. Let us go through methods implementation.
Constructor __init()__
. We will create and place aliens in a separate method spawn()
. Thus, our constructor will be very simple. You only need to store its only win
parameter as an attribute, initialize __pos
to some predefined value (I’ve picked (0, 0.5)
but you can always optimize it later on), initialize aliens
attribute to an empty list, and call spawn()
method.
spawn()
. Here, create aliens on 7×3 grid with a step of 0.2 units centered at AlienArmada
’s position. All aliens go to aliens
attribute, of course. It should look like this:
draw()
. You simply draw all aliens.
Put your updated code into exercise05.py, plus create the AlienArmada
class.
17.10 A laser shot appears
Now we need to create Laser
class (in a separate file). Eventually, when a player presses left mouse button, the laser should appear at the location just above and fly up with a certain speed. Let us take care of the first step. Create Laser
class as a descendant of visual.ImageStim
(just like our Spaceship
and Alien
classes). It should take win
and pos
as arguments and call the ancestor constructor. The pos
argument will hold the position of the ship but the laser must appear above it, so you need to adjust pos[1]
for that. Hint, you can get window height from win.size[1]
and you know sprites are 32 pixels high. The algebra should be straightforward.
For testing, create a laser object right after you create the ship itself and draw it in the main loop. It won’t move but you will be able to see whether you’ve got the height right.
Put your updated code into exercise06.py, plus create the Laser
class.
17.11 The laser shot flies
Let us add fly()
method to the laser. It will be very simple, every time it is called the laser should move upwards by some arbitrary value of units (I’ve used 0.05 but pick the speed you like). Once it flies off the screen, we should be mark it for deletion, as we won’t need it anymore. Thus, create a new attribute expired
and set to False
. In the fly()
method, move the laser only if laser has not expired
and, once you move it, check whether it is off the screen (its vertical position is 1 + half-height). If it is, mark it as expired.
Remember to call fly()
method in the main loop and see how the laser flies up and off the screen. Once it is gone, set a breakpoint to check that its expired
flag is indeed True
.
Put your updated code into exercise07.py, update the Laser
class.
17.12 We want more lasers!
Now we will create a LaserGun
class that will take care of individual laser shots. It will 1) create a new Laser
object whenever it is fire()
d, 2) ensure that it does not overheat by allowing only one shot every N seconds (let’s say 0.3), 3) call draw()
and fly()
methods for all laser shots, and 4) remove the expired ones via cleanup()
.
Create the class LaserGun
in the same file as the Laser
.
Constructor __init()__
. In the constructor, pass PsychoPy window variable and store it as an attribute (you will need it when you create a new laser object), initialize lasershots
attribute with empty list, and create a timer
attribute via clock.CountdownTimer()
(decode on the minimal interval between the shots, I’ve picked 300 ms). We will use it inside the fire()
method later on.
fire(self, pos)
. It should take pos
tuple with position of the ship as a single parameter. If enough time passed since the last shot (check .timer
for that), create a new Laser
at the supplied position, add it to the lasershots
list, and reset the timer.
draw()
and fly()
simply draw or fly all lasershots
.
cleanup()
. This is both simple and tricky! Conceptually simple: you loop over lasershots
and delete any object that is expired
. You can do it via list comprehension (deleting objects by not including them in the updated list) but for didactic reasons we will use del instead. If you have a list and you want to delete a second element, you write
= [1, 2, 3, 4, 5]
x del x[1]
print(x)
## [1, 3, 4, 5]
However, there is a catch. Imagine that you want to delete second and forth elements, so that the result should be [1, 3, 5]
. If you just delete second and then forth elements, you won’t get what you want:
= [1, 2, 3, 4, 5]
x del x[1]
del x[3]
print(x)
## [1, 3, 4]
Do you see why? Solution: start deleting from the end, this way indexes of earlier elements won’t be affected:
= [1, 2, 3, 4, 5]
x del x[3]
del x[1]
print(x)
## [1, 3, 5]
Note that you must use del list[index]
format, so you need to use indexing for loop for the cleanup()
function:
for ishot in backwards-index-built-via-range-function:
if self.lasershots[ishot] needs to be deleted:
self.lasershots[ishot] delete that
For testing, use LaserGun
in place of the Laser
. Both have same draw()
and fly()
methods that you should be calling already. Add cleanup()
call to the main loop right after the fly()
(that will automatically delete expired shots). Also in the main loop, check if left mouse button is pressed. If it is, fire()
the laser gun, passing current position of the ship to it. Once you fired a few shots and they are off the screen, put a breakpoint and check that the .lasershots
is empty (you cleanup()
works as it should).
Put your updated code into exercise07.py, update the Laser
class.
17.13 I am hit! I am hit!
Now we need to check whether a lasershot hit an alien. In that case, all the relevant aliens and shots must be removed. It is easy for lasershots, we just set them as expired
and cleanup()
does the rest. Clearly, we need the same mechanism for the aliens.
Add teleported
attribute to the Alien
class and initialize to False
. In AlienArmade
class, implement a cleanup()
method, analogous to one in LaserGun
that will delete any teleported
aliens. Add the call to it in the main loop at the same location as for the LaserGun
. You can test it by setting teleported
to True
for one of the aliens and it should be missing.
Now for the actual check. You should loop over all aliens and laser shots checking whether whether they overlap. If they do, set laser shot to expired
and the alien as teleported
. Do this check before the clean up but after the laser shots fly.
Test it!
Put your updated code into exercise08.py, update Alien
and AlienArmade
classes.
17.14 Duck typiing iterators
In the precious exercise, you used aliens
and lasershots
attributes directly in the loop. However, let us inject some coolness into our code and turn AlienArmada
and LaserGun
classes into iterators. Recall that you need to implement two special methods for this: __iter__(self)
and __next__(self)__
. The first one, initializes the loop, the second one yields the next item. The actual implementation is very simple. You need to create a new attribute that will be used to track which item you need to yield, call it iter
. Initialize it to None
in the constructor.
In the __iter__(self)
, initialize the counter to 0
and return self
, returning the reference to the iterator, which is the object itself49. In the __next__(self)
, check if iter
is within the list length. If it is not (i.e., you ran out aliens/shots to iterate over), raise StopIteration()
. If it is, increase the iteration counter and return the element it was indexing before that: Think about why you need to return self.aliens[self.iter-1]
rather than self.aliens[self.iter-1]
or how you can use a different starting value and range check to avoid this.
Do this for both classes and use them in for loops directly, instead of aliens.aliens
and laser.lasershots
.
Test it! It should work as before, of course, but with cooler duck typing inside!
Put your updated code into exercise09.py, update LaserGun
and AlienArmade
classes.
17.15 Got’em all!
Now, implement __len()__
method for AlienArmada
class that so returns the number of remaining aliens. In the main loop, use it via len()
function to check whether you won: no aliens left, the game is over. Use len()
with the object, not with the list attribute!
Put your updated code into exercise10.py, update LaserGun
and AlienArmade
classes.
17.16 Ping!
Now let us use a TeleportSoundMixin
class that defines a single teleport()
method that plays the teleport.wav sound (created by Sergenious and obtained from freesound.org) is in materials. Thus, it can be a single line method when you both create and play the sound in one go, no temporary variable or an attribute are required. Then, use it as an ancestor for the Alien
class. Now it has that teleport()
method. Call it when you set teleported to True
.
Put your updated code into exercise11.py, create TeleportSoundMixin
class and update Alien
class.
17.17 The alien armada jumps
Currently, our aliens are sitting ducks. No fun! They need to get a move on. But before we can move alien armada around, we need to be able to change its position. Recall that we have __pos
attribute in AlienArmada
class. I’ve made it (kinda) private via __pos
on purpose, so we can define getter and setter methods for it and call the virtual attribute pos
to make it consistent with the rest. If you forgot what getter/setter methods are, read again.
To start with, define a getter. It only needs to return the value of the hidden position attribute.
Now, to more complicated and, hence, fun bit! Remember, we need to move all individual aliens relative to the center of the armada. For this, 1) compute the change in position, 2) use it to alter position of individual aliens based on their current position, 3) store the new position in the hidden attribute.
Test it by making armada jump to a random location every time you press space button. The armada should keep its formation!
Put your updated code into exercise12.py, update AlienArmada
class.
17.18 The alien armada moves
Our alien armada will move downwards along a sine trajectory. Thus, we need to define a fly()
method that will move it downwards by some small step (I’ve used 0.001, which is quite fast for 60 Hz, as it will move 0.06 units per second). And the horizontal position will be computed as a sine wave. The general formula is:
\[x = x_{max} \cdot sin(2 \pi f \cdot (y-y_{origin})) \]
where \(x_{max}\) is maximal deviation of the armada from the middle of the screen (I’ve set it to 0.25), the \(f\) is the frequency, i.e., how fast is horizontal movement (I’ve set to 3), \(y\) is the current vertical coordinate of the armada and \(y_{origin}\) is the initial one (0.5 in my case). Experiment with values to see how they affect the movement.
Once you computed both components, assign the tuple to the pos
and it will move the entire armada. In the main loop, call fly()
when you call it for laser gun.
Put your updated code into exercise13.py, update AlienArmada
class.
17.19 The alien armada wins: crash!
So far, the player always wins. Let us make it more dangerous! They will lose if either an alien crashes into the spaceship or if the armada goes past the ship. Implement the former the same way as you checked whether laser shot hit an alien: loop of aliens and see if the overlap with the spaceship (game over if they do). Implement and test.
Put your updated code into exercise14.py.
17.20 The alien armada wins: missed them!
The second way to lose the game, is if at least one alien got past the ship. For this, we need to check the position of the lowest alien and if it is lower than that of the spaceship, the game is over. You can do it in the same loop you are checking for the hit but, for didactic reasons, let us practice @proprety
a bit more. Implement a new read-only property lowest_y
of the AlienArmada
class that will return the y-coordinate of the lowest alien in the armada. You need to loop over individual aliens to find the lowest y and return it. There are different way to do it, come up with one yourself! In the main code, add the check and make sure the game is over, if aliens got past the player.
Put your updated code into exercise15.py, update AlienArmada
class.
17.21 Mixin teleport
Mix in the teleport sound method to the Spaceship
and play it if alien crashed into it.
Put your updated code into exercise16.py, update Spaceship
class.
17.22 Game over message
Create a game over message (a blinking one would be cool but this is up to you) that will reflect the outcome. Something like this but use your imagination:
- “Coward!”, if the player pressed escape.
- “Congrats!”, if the player won.
- “Crash! Boom! Bang!”, if the player was hit.
- “They got away!”, if the armada got past the player.
One way to simplify you life is to rename gameover
variable into the gamestate
variable. Initially, it could be "playing"
, so your loop repeats while gamestate == "playing"
. Then, you can set it based on the type of the event, e.g., "abort"
for escape button press, "victory"
when player wins, etc. and use it after the main loop to determine which message to show. Better still, use dictionary for this instead of if-else. Or “cheat” and store the message in the variable itself.
Put your updated code into exercise17.py.
17.23 We want more!
You have a solid game but it can be improved in many ways. Currently, we move all game object assuming constant frame rate. That might not be the case, so it would be wiser to use inter-frame timer and use time elapsed since the last update. And it can use more feature. Score? Difficulty? More levels? Aliens shooting back at you? Anything is possible!
boilerplate↩︎
A man is hailing a taxi on a sidewalk. A car stops next to him. The guy looks at it and says “But there is the taxi sign on the roof?”. The driver replies: “Do you need a taxi sign on the roof or a ride?”↩︎
Why not implement it as a method
object.len()
or, even better, as a read-only propertyobject.len
? Read here for the justification.↩︎Lazy computation means that you compute or get only what is necessary right now, rather than computing or getting all items in one go.↩︎
This is what function
range()
returns, not a list but an iterator object.↩︎Of course, if you do need it, you should implement it. The point is that quite often you do not.↩︎
Number of submissions is unlimited with reason.↩︎
No worries, it is safe. He says, you should understand. But I have no idea because, evidently, I am out of touch with modern trends.↩︎
You can cheat and return the reference to the list attribute itself and it’ll do the rest. Don’t do it for didactic purposes.↩︎