P-230 - Shields
Python Asteroids+Invaders on GitHub
The Invaders game has shields, four of them. They take damage from invader shots, but are not destroyed in one go. This will be interesting. What do we need and what are some small steps?
Perhaps most interesting, not mentioned in the blurb, is that if you have a two-player game, which we haven’t even really contemplated, each player’s shields are saved when they are hit and restored as they were on that player’s next turn. What is my plan for that, you ask? I intend to do nothing about it during the single player game work. If and when we do two players, we’ll deal with it then.
Is this incredibly imprudent? Are we likely to be in big trouble because we did not provide for this feature? I doubt it. We’ll wait and see.
Here’s what we know about what we need:
- There are four shields;
- Each one takes its own damage when hit;
- Both invader shots and player shots damage a shield;
- The damage is taken where the shot hits and looks kind of random and ragged;
- If a hole in a shield is big enough, shots can go right through;
What are some possible small steps?
- Get one shield on the screen;
- Get all four on the screen;
- Shots ignore shields;
- Shots die upon hitting shields;
- Shots do simple damage;
- Shots do fancy damage;
The overall rule will be that we can release the shield code frequently, without breaking the build or the game, subject only to the fact that the shields only have whatever capability they have, such as in the list above.
What about tests? I freely grant that my feeling is that I could do this just fine with very few tests, because whatever we do will be so visible. So what I’ll require myself to do is to at least build a test suite for Shield and try to pop in a test when I can. There will perhaps be things to test in detecting collisions, though I expect the Collider will “just work”, and there might be tests we can write for doing damage.
I have to say that for damage, I think we’ll just be turning off bits in the Shield, and that the visual effect will be easy to see and it will be nigh on to impossible to test it with a code test. We’ll see.
Let’s make the test file, demand the object, create the class, and (sadly) update all the other classes to have the interaction. I hate that part even though I learned yesterday that it’s not hard to do. This time through, I’ll time how long it takes to do the update. We’ll commit as soon as the initial test is done, the class exists, and all the others are updated. I’ll keep some time hacks:
0619-0620
class TestShield:
def test_exists(self):
Shield()
0621-0622
from flyer import InvadersFlyer
class Shield(InvadersFlyer):
pass
Tests are failing. My new one says:
def test_exists(self):
> Shield()
E TypeError: Can't instantiate abstract class Shield with abstract methods draw, interact_with, interact_with_bumper, interact_with_invaderexplosion, interact_with_invaderfleet, interact_with_invaderplayer, interact_with_invadershot, interact_with_playerexplosion, interact_with_playershot, interact_with_shotcontroller, interact_with_shotexplosion, interact_with_topbumper, mask, rect, tick
And my checker test that checks to be sure everyone implements things is failing.
First I’ll let PyCharm put all the methods into my new class.
0625-0626
I do that and the new test passes. The old one doesn’t, but I’m not going to worry about it yet. Let’s see what the Code / Analyze tells me.
0627-0628
Nothing happens, because I didn’t add the new abstract method to InvadersFlyer yet. Right.
@abstractmethod
def interact_with_shield(self, shield, fleets):
pass
Now lots of tests fail. Perfect. Do the Analyze
0629-0633
All the insertions are done. PyCharm is very helpful. If I didn’t want to pick where the code was inserted, it would be faster, but it would put the new method at the top of the class and that’s the worst possible place in my view.
Tests are still failing. I’m surprised. Ah. PyCharm didn’t run them. There’s just the one checker test:
0637ish
I am not sure why my special test is failing. This will take some time, I’m afraid.
Oh I see part of the issue: I suspect that Pytest does not munge assertions that are outside of methods whose names begin with test
and so my messages are not coming out.
0644
That took some editing. I had over-estimated how clever Pytest was. It’s not quite as clever as I might have hoped. The test was failing correctly, telling me that Shield does not implement interact_with
:
0647
class Shield(InvadersFlyer):
def interact_with(self, other, fleets):
other.interact_with_shield(self, fleets)
We are green. Commit: First commit of Shield class, unused.
That was a massive commit of 13 files, all trivial changes, tests still green. Let’s reflect.
Reflection at 0650
The actual work of test-driving the existence of the class, implementing the class, and bringing all the methods up to date took only eight or nine minutes. Updating that improperly implemented test took some time but in fact when I made it tell me what it was complaining about it was correct to complain.
However, I am again questioning the abstract methods. Yes, declaring interact_with_shield
as abstract did force me to implement it in all InvadersFlyer subclasses. If it were not abstract, I might have forgotten.
But I did not think out for each class what it should do and allow pass
only on those that should pass (almost everything) and something else, maybe assert False
for the ones that do need to interact. So the abstract method hasn’t bought me anything, it has just taken up time and space.
While we’re reflecting, which classes do need to interact with shields? InvaderShot, PlayerShot … and Invader(!) When an invader hits the shields, he basically shaves them away, eating right through. I’m glad we had this little chat because I might have forgotten. Those interactions go both ways of course, x vs Shield and Shield vs x.
So in those three classes, right now before I forget … what should I do? If I assert False, I’ll break the game until there is actual code there to deal with that collision. I can’t do them all at once. I could print a warning to the console, but that seems seriously off.
These abstract methods aren’t really buying me what I thought they might, the inability to forget to implement an interaction. The only way to ensure that I remember to hook up invader vs shield will break the game until I make it work, and I can’t make all of them work at once. I would not be able to commit incrementally.
If this were a publish-subscribe model, I think we wouldn’t be having this discussion. We would publish our new message “interact with shield”, and no one would subscribe and there would be no errors, warnings or other hoorah. Then one object would subscribe and do something. We’d commit, rinse ,repeat.
Must think on this and confer with my fellow wizards.
For now, let’s get started doing something. Let’s get a shield on the screen. That’s visual and I think I’ll do it visually.
I just code this much:
class Shield(InvadersFlyer):
def __init__(self, position):
maker = BitmapMaker.instance()
self.map = maker.shield.copy()
self._mask = pygame.mask.from_surface(self.map)
self._rect = self.map.get_rect()
self._rect.center = position
@property
def mask(self):
return self._mask
@property
def rect(self):
return self._rect
def draw(self, screen):
screen.blit(self.map, self.rect)
Now in Coin, let’s create one and see what breaks.
def invaders(fleets):
fleets.clear()
fleets.append(Bumper(64, -1))
fleets.append(Bumper(960, +1))
fleets.append(TopBumper())
fleets.append(InvaderFleet())
fleets.append(InvaderPlayer())
fleets.append(ShotController())
fleets.append(Shield(Vector2(100, 900)))
I have no idea where they go, I’ll have to read the Invaders pages. I just picked (100, 900) and get this:
Commit: Shield on screen in wrong place.
Reflection
One down, N to go:
-
Get one shield on the screen; - Get all four on the screen;
- Shots ignore shields;
- Shots die upon hitting shields;
- Shots do simple damage;
- Shots do fancy damage;
Now the fact is that we have more than one down. We do have at least one other commit and we have a bunch of files updated and methods stubbed out. If I’d been forced to estimate #1 there, would I have remembered those? We had a surprise broken test that took about twice as long as the actual work. I certainly wouldn’t have known to estimate that.
And if I had some nasty-#!@ boss who hassled me about estimates? Would I have cut corners? You know it. There must be a lesson in there somewhere.
Something I realized in the doing of getting the first shield to display was that I needed to copy the bitmap. You may have noticed that in the init. But I also realized that my mask can’t really be static. After there’s damage to the shield, we need to reflect that in the mask, because that’s where we do collisions. That’s somewhere around item #4 in the list above. I think I could write a test for that but I can’t write it now. What I can do, however, is implement a null test to remind me:
class TestShield:
def test_exists(self):
Shield(Vector2(0, 0))
def test_mask_updates_after_shield_hit(self):
pass
Since that’s the whole test file, I probably won’t miss that. I wonder if there is a way to do a warning in Pytest … yes. I can put an ignore on there and that will show up in the display of test results.
@pytest.mark.skip(reason="needs work")
def test_mask_updates_after_shield_hit(self):
pass
OK. Now I get the popup that says “Tests ignored: 1, passed: 204”.
This is a good time to break. I’ve been at this for nearly two hours, including writing and a break or two, and my next steps will go better if I put the shields where they belong. I’d better make sure that the Player is where it belongs as well. It does seem kind of high up on the screen.
Summary
The class itself went in easily. All the hassle came from the boilerplate. That’s an issue.
More learning about the abstract class idea: it’s just not serving. I am inclined to go back to no abstract methods other than the basic ones. None for the interact_with_xyz
, just the basics, interact_with
, maybe a few others that are too easy to forget. Since the interaction matrix is so sparse, defaulting to nothing may be the best choice?
You are waffling, sir!
Yes, I am waffling. The tensions don’t want to balance. I’m feeling:
- Angel: You’re not supposed to inherit concrete methods;
- Devil: But these are just
pass
; - Angel: Yes, but you’re not supposed to inherit concrete methods;
- Devil: But these clean up the code so nicely;
- Angel: Yes, but the abstracts prevent errors;
- Devil: But not the errors in the
interact_with_xyz
, they’re really no help; - Angel: You’re just not concentrating enough;
- Devil: Right, like I’m going to get smarter all of a sudden;
- Angel: My point exactly;
- Devil: They’re still not helping;
- …
I need a better idea. Until I get one I may waffle but if I’m going to waffle I think I want to waffle with less code and fewer restrictions. We’ll see.
See you next time!