Kotlin 157: Catch Up Ball
I’d like to get my version of this thing closer to the state of Hill’s, so that whatever changes he comes up with are easier to assess, and to learn from.
In aid of that, I think the next things will be to break out the rest of SolidObject instances into their own classes. That should give me a chance to reduce the breadth of the SolidObject class and its interface. I extracted the interface a while back, with some good idea in mind about making it easier to remove the default methods and see what I liked and didn’t about the result. So far, it has neither helped nor hindered.
Another thing that needs to be done is to put the before and after interaction calls into the Interactions object. Perhaps finalize will go there as well.
Nothing for it this morning but to get started, I guess.
SolidObject now has only three companion methods for constructing things:
companion object {
fun asteroid(
pos: Point,
vel: Velocity = U.randomVelocity(U.ASTEROID_SPEED),
killRad: Double = 500.0,
splitCount: Int = 2
): SolidObject {
return SolidObject(
position = pos,
velocity = vel,
killRadius = killRad,
isAsteroid = true,
view = AsteroidView(),
finalizer = AsteroidFinalizer(splitCount)
)
}
fun ship(pos: Point, control: Controls = Controls()): SolidObject {
return SolidObject(
position = pos,
velocity = Velocity.ZERO,
killRadius = 150.0,
view = ShipView(),
controls = control,
finalizer = ShipFinalizer()
)
}
fun shipDestroyer(ship: SolidObject): SolidObject {
return SolidObject(
position = ship.position,
velocity = Velocity.ZERO,
killRadius = 99.9,
view = InvisibleView()
)
}
}
To warm up, I guess I’ll start with ShipDestroyer, since it has almost no behavior. Let’s also review some of the ones that are broken out already, in particular Missile.
class Missile(
ship: SolidObject,
): ISpaceObject, InteractingSpaceObject {
var position: Point
val velocity: Velocity
val killRadius = 10.0
var elapsedTime: Double = 0.0
val lifetime: Double = 3.0
init {
val missileKillRadius = 10.0
val missileOwnVelocity = Velocity(U.SPEED_OF_LIGHT / 3.0, 0.0).rotate(ship.heading)
val standardOffset = Point(2 * (ship.killRadius + missileKillRadius), 0.0)
val rotatedOffset = standardOffset.rotate(ship.heading)
position = ship.position + rotatedOffset
velocity = ship.velocity + missileOwnVelocity
}
override fun update(deltaTime: Double, trans: Transaction) {
elapsedTime += deltaTime
if (elapsedTime > lifetime) {
trans.remove(this)
}
position = (position + velocity * deltaTime).cap()
}
override fun beforeInteractions() {
}
private fun weAreCollidingWith(other: ISpaceObject) = weCanCollideWith(other)
&& weAreInRange(other)
private fun weCanCollideWith(other: ISpaceObject): Boolean = (other is SolidObject)
&& other.isAsteroid
private fun weAreInRange(other: ISpaceObject): Boolean = other is SolidObject
&& position.distanceTo(other.position) < killRadius + other.killRadius
override fun afterInteractions(trans: Transaction) {
}
override fun draw(drawer: Drawer) {
drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
drawer.translate(position)
MissileView().draw(this, drawer)
}
override fun finalize(): List<ISpaceObject> {
return listOf(Splat(this))
}
override val interactions: Interactions = Interactions(
interactWithSolidObject = { solid, trans ->
if (weAreCollidingWith(solid)) {
trans.remove(this)
trans.remove(solid)
}
}
)
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
other.interactions.interactWithMissile(this, trans)
}
}
There’s a lot here that needs doing. In particular, we have to deal with collisions in each of these SolidObject subclasses. I think I’ll let it duplicate, at least once, rather than try anything clever along the way of pulling out the class. Once we have common elements in the classes, we can explore how to combine them.
Now, I’m not sure this is a good idea but what I think I’ll do is to change the shipDestroyer
companion method to create a ShipDestroyer instance, and see what breaks. This might lead to chaos, but there’s a decent chance that we can just follow our nose to get it done.
fun shipDestroyer(ship: SolidObject): SolidObject {
return SolidObject(
position = ship.position,
velocity = Velocity.ZERO,
killRadius = 99.9,
view = InvisibleView()
)
}
I’ll just change that to refer to ShipDestroyer and let IDEA take the lead for a bit.
fun shipDestroyer(ship: SolidObject): SolidObject {
return ShipDestroyer(
position = ship.position,
velocity = Velocity.ZERO,
killRadius = 99.9,
view = InvisibleView()
)
}
IDEA notices the need for a new class. With my help, we get here:
class ShipDestroyer(
position: Point,
velocity: Vector2,
killRadius: Double,
view: InvisibleView
) : ISpaceObject, InteractingSpaceObject {
}
I think we need most of those members to be visible, but we don’t need to set them externally.
Let’s do this:
class ShipDestroyer(
position: Point,
) : ISpaceObject, InteractingSpaceObject {
val velocity = Velocity.ZERO
val killRadius = 999
val view = InvisibleView()
}
And in the companion:
fun shipDestroyer(ship: SolidObject): ShipDestroyer {
return ShipDestroyer(
position = ship.position
)
}
So far so good. IDEA wants the class to have all the necessary methods. Let it provide. Now the class looks like this:
class ShipDestroyer(
position: Point,
) : ISpaceObject, InteractingSpaceObject {
val velocity = Velocity.ZERO
val killRadius = 999
val view = InvisibleView()
override fun update(deltaTime: Double, trans: Transaction) {
TODO("Not yet implemented")
}
override fun beforeInteractions() {
TODO("Not yet implemented")
}
override fun afterInteractions(trans: Transaction) {
TODO("Not yet implemented")
}
override fun draw(drawer: Drawer) {
TODO("Not yet implemented")
}
override fun finalize(): List<ISpaceObject> {
TODO("Not yet implemented")
}
override val interactions: Interactions
get() = TODO("Not yet implemented")
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
TODO("Not yet implemented")
}
}
Let’s do the interactions bit first.
override val interactions: Interactions = Interactions()
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
other.interactions.interactWithShipDestroyer(this)
}
Now interactWithShipDestroyer
is red, because we need this:
class Interactions (
val interactWithScore: (score: Score, trans: Transaction) -> Unit = { _,_, -> },
val interactWithSolidObject: (solid: SolidObject, trans: Transaction) -> Unit = { _,_, -> },
val interactWithMissile: (missile: Missile, trans: Transaction) -> Unit = { _,_, -> },
val interactWithShipChecker: (checker: ShipChecker, trans: Transaction) -> Unit = { _,_, -> },
val interactWithShipMaker: (maker: ShipMaker, trans: Transaction) -> Unit = { _,_, -> },
val interactWithWaveMaker: (maker: WaveMaker, trans: Transaction) -> Unit = { _,_, -> },
val interactWithShipDestroyer: (destroyer: ShipDestroyer, trans: Transaction) -> Unit = { _,_, -> },
)
I’ve developed the habit of double-clicking the red interactWith
method name, going to Interactions, duplicating the last line, pasting the new method name, and editing the first parameter. Quick and easy.
Let’s see what else we need in ShipDestroyer. It doesn’t need to do anything on draw, doesn’t need to finalize. Empty those:
Truth is, I think it doesn’t need to do anything on before/after either … in fact it doesn’t need to do anything on update either. Furthermore, it doesn’t need position, velocity or anything. So now I have this:
class ShipDestroyer() : ISpaceObject, InteractingSpaceObject {
override val interactions: Interactions = Interactions()
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
other.interactions.interactWithShipDestroyer(this)
}
override fun update(deltaTime: Double, trans: Transaction) {}
override fun beforeInteractions() {}
override fun afterInteractions(trans: Transaction) {}
override fun draw(drawer: Drawer) {}
override fun finalize(): List<ISpaceObject> { return emptyList() }
}
}
Why doesn’t ShipDestroyer need position and such? Because we now are going to have a new function on ship, interactWithShipDestroyer. When we’re done, the existence of a ShipDestroyer will suffice to destroy the ship, no matter where the destroyer is. In the interim, we’ll have to do some checking in SolidObject so that we don’t destroy everything.
- Aside
- When Hill and I were pairing, when I’d run the game to test play, I was doing terribly: I just couldn’t seem to hit anything. He suggested that I needed a massive bomb that destroyed everything so that we could get to the second wave. With a little carelessness, ShipDestroyer could do that.
We do want ShipDestroyer to remove itself. Any interaction will do. We’ll use SolidObject for now.
override val interactions: Interactions = Interactions(
interactWithSolidObject = { _, trans ->
trans.remove(this)
}
)
That’s odd, isn’t it? We create a ShipDestroyer, and first thing it does is destroy itself. Now I think things would work now, with one exception: the ship won’t ever be destroyed exiting hyperspace. (And probably some tests will break. I want to make one more necessary change.)
override val interactions: Interactions = Interactions(
interactWithSolidObject = { solid, trans ->
if (weAreCollidingWith(solid)) {
trans.remove(this)
trans.remove(solid) // TODO: should be able to remove this but a test fails
}
},
interactWithShipDestroyer = {_, trans -> trans.remove(this)}
)
If there’s a ship destroyer in the universe …
override val interactions: Interactions = Interactions(
interactWithSolidObject = { solid, trans ->
if (weAreCollidingWith(solid)) {
trans.remove(this)
trans.remove(solid) // TODO: should be able to remove this but a test fails
}
},
interactWithShipDestroyer = {_, trans ->
if (this.isShip()) trans.remove(this)}
)
private fun isShip(): Boolean = this.killRadius == 150.0
I do want to do one more thing. I want a print when a ship destroyer is created, because hyperspace death is probabilistic and I don’t want to go a long time wondering if this works.
I didn’t do my boilerplate right:
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
other.interactions.interactWithShipDestroyer(this, trans)
}
Forgot to pass the trans through. Test the game, I can’t wait. A couple more typos, and the game runs, the ship destroyer message comes out, and the ship dies with a splat. As intended. Now let’s see what tests we have broken. None, zero, not any. Commit: ShipDestroyer broken out as separate ISpaceObject.
Commit gives me a warning I’d like to have seen before, pointing out that the creation of the shipDestroyer companion function requires no ship now. So in fact, let’s search out all the uses of that function and just do a ShipDestroyer instead.
private fun destroyTheShip(trans: Transaction) {
println("creating destroyer")
trans.add(ShipDestroyer())
trans.add(Splat(ship))
}
And then there’s this one:
fun control(ship: SolidObject, deltaTime: Double, trans: Transaction) {
if (hyperspace) {
hyperspace = false
recentHyperspace = true
trans.addAll(listOf(SolidObject.shipDestroyer(ship)))
}
turn(ship, deltaTime)
accelerate(ship, deltaTime)
trans.addAll(fire(ship))
}
When we go into hyperspace, we use a ShipDestroyer to do the job. Interesting … I had forgotten that. Remove the references to ship in ShipDestroyer construction. Remove the helper in companion. Commit and push “ShipDestroyer broken out as separate ISpaceObject”.
So that went nicely. Let’s review the class as a whole1.
class ShipDestroyer() : ISpaceObject, InteractingSpaceObject {
override val interactions: Interactions = Interactions(
interactWithSolidObject = { _, trans ->
trans.remove(this)
}
)
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
other.interactions.interactWithShipDestroyer(this, trans)
}
override fun update(deltaTime: Double, trans: Transaction) {}
override fun beforeInteractions() {}
override fun afterInteractions(trans: Transaction) {}
override fun draw(drawer: Drawer) {}
override fun finalize(): List<ISpaceObject> { return emptyList() }
}
I rather like that all those methods turn out to want to be null, and I dislike the fact that finalize
is different, because it returns a list. We could take this occasion to make it accept a transaction, which you fill in if you care to. That would let all the finalize folks who do nothing take out their return emptyList()
.
- Warning
- This was a bad idea. Scan down to “Revert”.
interface ISpaceObject: InteractingSpaceObject {
fun update(deltaTime: Double, trans: Transaction)
fun beforeInteractions()
fun afterInteractions(trans: Transaction)
fun draw(drawer: Drawer)
fun finalize(): List<ISpaceObject>
}
Change signature and release the Kraken of Chaos:
fun finalize(trans: Transaction)
Now all the senders would like to be revised.
In SpaceObjectCollection, we do this:
fun removeAndFinalizeAll(moribund: Set<ISpaceObject>): Boolean{
val trans = Transaction()
moribund.forEach {it.finalize(trans) }
applyChanges(trans)
}
This is making me go too deep too fast. I’ll try to bull it through. The Finalizers all return a list of objects to be added. I can let that ride, and deal with the transaction in implementors of finalize
elsewhere, for example:
class SolidObject
override fun finalize(trans: Transaction) {
val adds = finalizer.finalize(this)
trans.addAll(adds)
}
REVERT!!
This got too deep, plus, perhaps fortunately, I was interrupted. I’ve reverted all this. It was a digression anyway, but I thought it’d be easy. I was mistaken: there are a lot of test relying on finalize
returning a list. We could hack it to accept a transaction and return the list but there’s more going on here than I’d like to deal with just now.
Let’s see about breaking out another solid object. That actually went rather well last time. What’s left?
Break Out Asteroid2
Asteroid, and ship. Asteroid is easier, let’s do that. I’ll begin the same way, just creating it in the companion:
fun asteroid(
pos: Point,
vel: Velocity = U.randomVelocity(U.ASTEROID_SPEED),
killRad: Double = 500.0,
splitCount: Int = 2
): SolidObject {
return SolidObject(
position = pos,
velocity = vel,
killRadius = killRad,
isAsteroid = true,
view = AsteroidView(),
finalizer = AsteroidFinalizer(splitCount)
)
}
That can become
fun asteroid(
pos: Point,
vel: Velocity = U.randomVelocity(U.ASTEROID_SPEED),
killRad: Double = 500.0,
splitCount: Int = 2
): ISpaceObject {
return Asteroid(
position = pos,
velocity = vel,
killRadius = killRad,
isAsteroid = true,
view = AsteroidView(),
finalizer = AsteroidFinalizer(splitCount)
)
}
I think we’ll get rid of a lot of those parameters but we’ll start here. IDEA wants to make me a class and fill it in. That went well last time. But I really don’t want all those parameters. Let’s remove a few.
return Asteroid(
position = pos,
velocity = vel,
finalizer = AsteroidFinalizer(splitCount)
)
Let IDEA loose:
class Asteroid(
position: Point,
velocity: Velocity,
finalizer: AsteroidFinalizer
) : ISpaceObject, InteractingSpaceObject {
override fun update(deltaTime: Double, trans: Transaction) {
TODO("Not yet implemented")
}
override fun beforeInteractions() {
TODO("Not yet implemented")
}
override fun afterInteractions(trans: Transaction) {
TODO("Not yet implemented")
}
override fun draw(drawer: Drawer) {
TODO("Not yet implemented")
}
override fun finalize(): List<ISpaceObject> {
TODO("Not yet implemented")
}
override val interactions: Interactions
get() = TODO("Not yet implemented")
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
TODO("Not yet implemented")
}
}
We’ll start again with the interactions.
override val interactions: Interactions = Interactions()
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
other.interactions.interactWithAsteroid(this, trans)
}
This demands the new interaction:
val interactWithAsteroid: (asteroid: Asteroid, trans: Transaction) -> Unit = { _,_, -> },
Let’s deal with the asteroid interactions. It can interact with a missile, which wants to make it split if the missile is close enough. If it were to interact with another asteroid, nothing should happen, so we won’t implement that. And, since ships are currently the sole remaining SolidObject, we need to interact with those. However, we can at least take advantage of the fact that we know we’re dealing with a Ship.
override val interactions: Interactions = Interactions(
interactWithMissile = { missile, trans ->
if (weAreCollidingWith(missile)) {
trans.remove(this)
trans.remove(missile) // TODO: should be able to remove this but a test fails
}
},
interactWithSolidObject = { ship, trans ->
if (weAreCollidingWith(ship)) {
trans.remove(this)
trans.remove(ship) // TODO: should be able to remove this but a test fails
}
}
)
Those TODO are copied from SolidObject. There’s a test that doesn’t work. We’ll wind up fixing it but for now we’ll do the redundant removes. In the end, everything should concern itself with itself, and not with the other guy. Now we need to implement weAreCollidingWith
, probably twice.
We have some implementations but we can do it more simply now. We used to have to check to see if collisions with the other object were possible, basically a type check. Now the Interactions have sorted that out. We know we can collide with a missile, we just want to know if we are too close.
private fun weAreCollidingWith(missile: Missile): Boolean {
return position.distanceTo(missile.position) < killRadius + missile.killRadius
}
We seem to need a position to refer to. We declare the position parameter var
. And we need to know our killRadius. We’ll set it to 500 for now.
I have too many balls in the air. I’m thrashing just a bit. I think I shouldn’t have removed those parameters so quickly. Put killRadius back. I’m not sure if I need it, would prefer to pick it up from the constructor for now:
class Asteroid(
var position: Point,
velocity: Velocity,
killRadius: Double,
finalizer: AsteroidFinalizer
) : ISpaceObject, InteractingSpaceObject {
In update we just need to move according to our velocity, which is fixed.
override fun update(deltaTime: Double, trans: Transaction) {
position = (position + velocity * deltaTime).cap()
}
Before and after are not of interest.
override fun beforeInteractions() { }
override fun afterInteractions(trans: Transaction) { }
We need draw.
- Aside
- My code is ahead of my words, because after I did draw and a couple of implied changes, I tried the game and more and more asteroids kept appearing. I finally realized the problem wasn’t with asteroid but with WaveChecker:
override val interactions: Interactions = Interactions(
interactWithSolidObject = { solid, _ ->
if (solid.isAsteroid) {
sawAsteroid = true
}
}
)
There are no solid objects that are asteroids. Now it needs to be:
class WaveChecker
override val interactions: Interactions = Interactions(
interactWithAsteroid = { _, _ -> sawAsteroid = true }
)
I’ve got Asteroid down to this now:
class Asteroid(
var position: Point,
val velocity: Velocity,
val killRadius: Double,
val splitCount: Int = 2
) : ISpaceObject, InteractingSpaceObject {
private val view = AsteroidView()
private val finalizer = AsteroidFinalizer(splitCount)
override fun update(deltaTime: Double, trans: Transaction) {
position = (position + velocity * deltaTime).cap()
}
override fun beforeInteractions() { }
override fun afterInteractions(trans: Transaction) { }
override fun draw(drawer: Drawer) {
drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
drawer.translate(position)
view.draw(this, drawer)
}
override fun finalize(): List<ISpaceObject> {
return finalizer.finalize(this)
}
fun scale() = finalizer.scale()
private fun weAreCollidingWith(missile: Missile): Boolean {
return position.distanceTo(missile.position) < killRadius + missile.killRadius
}
private fun weAreCollidingWith(solid: SolidObject): Boolean {
return position.distanceTo(solid.position) < killRadius + solid.killRadius
}
override val interactions: Interactions = Interactions(
interactWithMissile = { missile, trans ->
if (weAreCollidingWith(missile)) {
trans.remove(this)
}
},
interactWithSolidObject = { ship, trans ->
if (weAreCollidingWith(ship)) {
trans.remove(this)
}
}
)
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
other.interactions.interactWithAsteroid(this, trans)
}
}
Rather a lot of methods, but we’ll be clearing that up. I changed the interactions not to destroy the other, only the self, because with two-way interactions that is as it should be.
We’d better make sure that Missile does the right thing.
override val interactions: Interactions = Interactions(
interactWithSolidObject = { solid, trans ->
if (weAreCollidingWith(solid)) {
trans.remove(this)
trans.remove(solid)
}
}
)
SolidObject is only the ship, which just about can’t shoot itself down. It’s not fast enough to get in front of its own missiles. Let’s change that to asteroid and adjust as needed.
override val interactions: Interactions = Interactions(
interactWithAsteroid = { asteroid, trans ->
if (weAreCollidingWith(asteroid)) {
trans.remove(this)
}
}
)
The current implementation is:
private fun weAreCollidingWith(other: ISpaceObject) = weCanCollideWith(other)
&& weAreInRange(other)
private fun weCanCollideWith(other: ISpaceObject): Boolean = (other is SolidObject)
&& other.isAsteroid
private fun weAreInRange(other: ISpaceObject): Boolean = other is SolidObject
&& position.distanceTo(other.position) < killRadius + other.killRadius
Since we automatically know that we can collide (because asteroid) we only need to know if we’re in range. Let’s just inline it:
override val interactions: Interactions = Interactions(
interactWithAsteroid = { asteroid, trans ->
if (weAreInRange(asteroid)) {
trans.remove(this)
}
}
)
private fun weAreInRange(asteroid: Asteroid): Boolean
= position.distanceTo(asteroid.position) < killRadius + asteroid.killRadius
Try that. All works as intended except that the ship doesn’t die when an asteroid hits it.
I really want my tests back but I think they’re going to need too much revision. I just can’t bring myself to chase them now.
The ship collision will be in SolidObject:
override val interactions: Interactions = Interactions(
interactWithSolidObject = { solid, trans ->
if (weAreCollidingWith(solid)) {
trans.remove(this)
trans.remove(solid) // TODO: should be able to remove this but a test fails
}
},
interactWithShipDestroyer = {_, trans ->
if (this.isShip()) trans.remove(this)}
)
There is only ship in SolidObject now. We want interact with Asteroid, and we can simplify.
override val interactions: Interactions = Interactions(
interactWithAsteroid = { asteroid, trans ->
if (weAreInRange(asteroid)) trans.remove(this) },
interactWithShipDestroyer = {_, trans ->
if (this.isShip()) trans.remove(this)}
)
override fun weAreInRange(asteroid: Asteroid): Boolean {
return position.distanceTo(asteroid.position) < killRadius + asteroid.killRadius
}
Test in game. That works. But I noticed an asteroid breaking all on its own. I wonder if our safe return check is working. ShipMaker is flawed:
override val interactions: Interactions = Interactions(
interactWithSolidObject = { solid, trans ->
if ( solid.isAsteroid) asteroidTally += 1
safeToEmerge = safeToEmerge && !tooClose(solid)
}
)
This needs to deal with asteroids, not solids.
override val interactions: Interactions = Interactions(
interactWithAsteroid = { asteroid, trans ->
asteroidTally += 1
safeToEmerge = safeToEmerge && !tooClose(asteroid)
}
)
private fun tooClose(asteroid: Asteroid): Boolean {
return ship.position.distanceTo(asteroid.position) < U.SAFE_SHIP_DISTANCE
}
What is that tally in there? Ah, we use it to prime the hyperspace failure object. That’s fine. Test.
Game is fine. Now for the bad news: what tests are unhappy?
Ah, cleaning up the tests just required a few changes of interactWithSolidObject
to interactWithAsteroid
and a couple of checks that we counting deep in the collections had to check for Asteroid rather than SolidObject.
We are green and working. Commit: Asteroid is broken out from SolidObject.
I do have a few warnings. They have to do with name hiding in parameters for the finalizer and the fact that InvisibleView is no longer used. We’ll ignore those for now. They are harmless and when we’ve done Ship we’ll have a larger cleanup to do.
Let find all the users of our companion creator and convert them to direct Asteroid creation.
Irritatingly there are 16. Fortunately, after a quick adjustment to the helper function’s parameter list, they all come down to replacing “SolidObject.a” with “A”. Green. Commit: Companion function asteroid removed from SolidObject.
Let’s sum up.
Summary
One mistake that I made too often was that I was too quick to remove parameters from the creation of my new objects. That resulted in more work before I was ready to test. In my next and final trick, the Ship, I’ll try not to do that.
I really missed the tests, but felt that revising them would be too distracting, because most of the changes were going to be trivial. I think I’d have done better to revise them. That said, I don’t think they would have found the bugs that did occur.
A few defects arose due to objects not fielding interactWithAsteroid
, instead implementing interactWithSolidObject
. When Hill and I paired, we put in some calls to that method from classes where we knew the actual class. We might have done better to break out the type right away, even though we weren’t really ready for it. We’d have had to revise some calling sequences, perhaps, but we would have had all the right stuff in place for today.
This raises an issue in my mind. Interaction issues are still challenging, because when we create a new class, like Asteroid, we have to figure out all the objects that need to implement interactWithAsteroid
, because if we don’t, some interaction just won’t happen. We probably have most of the cases covered by tests, but if we missed one out, there might be an interaction that should happen on screen but never does.
An example was in the WaveChecker, which suddenly saw no asteroids, because it was watching SolidObjects and asteroids moved out of that class. So WaveChecker thought there were no asteroids and kept adding more in. That one was pretty easy to spot. But if the ship failed to implement interactWithAsteroid
, ship-asteroid collisions would never happen. We had that bug for a while, and I must say that the game is easier if the asteroids can’t destroy the ship.
The value of our Interactions class is that objects only get messages that they explicitly ask for. The value of fixed overrides is that you can’t forget one, the compiler reminds you. We’ve made our classes much simpler, but there is mistake of omission that we can make.
Maybe we need some kind of checker that produces a report of who doesn’t listen to whom. Maybe we’ll just be careful … but I never find that to be a satisfying solution to an all too human mistake.
We’ll see. Every design has pros and cons … this is one of our cons.
Overall, though …
The changes went in very smoothly, and many simplifications have shown up as the classes become more focused. And soon, we’ll be able to remove a lot of functions that we are presently required to override. It’s starting to look like the only thing one object wants to know about another, other than its type as reported in the interactWith
function, might be its position, so we can see whether it’s too close to us. We might be able to get around that by implementing a howFarAreYouFrom(aPoint)
method on each class, so that we don’t have to assume a position member.
We’ll see. I need the code and my mind to settle down before worrying about a detail that small.
Observation?
I think that when Hill programs, he builds up a stronger mental picture of what’s going on than I do. My own practice relies very much on looking at some particular place, improving it, and then dealing with the ramifications. I think he’d make himself more aware of the ramifications before starting. Next time we pair, he’s going to drive, so we might get some insight on that.
I have two good reasons for not trying to grok the whole program in fullness. First, and most important, my brain is old and tired and can’t remember three things all at the same time. Second, and my excuse is, when developers work with large bodies of code, they will not be able to remember or even discover all the ramifications, yet they must make progress. So I try to work in a bit of a haze of unknown. This is not difficult for me to simulate. But as we’ve seen, sometimes that backfires and I have to revert, or I spend a long time debugging something. That happens seldom, though often enough that I wish it were more infrequent.
I do better with smaller steps. However, I’m not sure how you replace one powerful object with another in smaller steps. Today was like that. It went well. We’ll settle for that.
Good progress today, two classes broken out from SolidObject. Just one more to go, and then I think we’ll see even more simplification.
See you next time!
- No, Wait!
- I think I can remove the
weAreCollidingWith
and related messages from the interface. That lets me removeweCanCollideWith
andweAreInRange
. I do so, removing a few individual implementations along the way. Green. Commit: remove unused weAreCollidingWith and subordinate functions. A small but nice improvement.
Now we’re done. See you next time!