Kotlin 110
I seem to be zeroing in on an asteroids kind of game in this exercise. That’s probably good, because I’ve done asteroids at least once before.
In most of the games I’ve done, several Spacewar, an Asteroids, a Space Invaders, I’ve always had difficulty getting good small tests. Much of what I’ve done has been to look at the screen and see how it’s going. There are issues with that approach, including but not limited to:
- Defects can slip in that are not noticeable on the screen in a short bit of game play.
- Refactoring is made more difficult, because there are no safety rails as provided by tests.
- Steps tend to drift toward larger, because we need to code until the game play reflects the change. This tends to create more defects and more time is lost to debugging.
So this time, I’m going to work carefully to test as much as I can with code tests, very small of course. I’ll try to resist even looking at the screen, and aside from timing, if I notice any problems on the screen, I’ll take it as a sign that I need to up my testing game.
This morning, I want to make the ship accelerate and possibly turn. I say possibly because it is Sunday and my time may be limited because there is a Sunday ritual involving bacon. That occurs at a time that I cannot predict.
So let’s get to it.
The game will probably be controlled by keystrokes on my keyboard, or conceivably by operating the mouse. Inconceivably, I might get a game controller and hook it up to my Mac, but that seems very unlikely unless some strange notion comes over me. For any of these, the issue is testing.
You can’t easily write an automated test that deals with the user pressing a key or moving the mouse. You can, of course, emulate those actions somehow. But if the code says something like “is W key down” and does something until W goes up … it gets hard to test.
My plan this morning is to have a ControlPanel for the ship, that abstracts the notions of what it can do. This may not be a great idea, since I was nearly asleep when I had it, but now I am nearly awake and I still like it. Essentially the idea is this:
The ship can only do four things. It can rotate left, rotate right, accelerate, and fire a missile. In the original game, these actions were controlled by four spring-loaded buttons, left, right, accelerate, fire. You probably had to press fire for each missile, but maybe it is continuous fire when held down. I don’t have an Asteroids machine here to tell me.
I propose to have a control panel that is given to the ship, that has four boolean states, left, right, accelerate, fire. Then in my tests, I can provide a testing control panel, and test whatever state or behavior I wish to test. In the “real” game, we can devise a keyboard control panel, or a mouse control panel, or a game controller one. Whatever.
Let’s get to it.
We’ll start with accelerate. The test will be to create a ship with zero velocity, cycle it, see that it has not moved, “hold down” the accelerate button, cycle the ship, see that is has moved and its velocity is now appropriate. We might do more, but it’ll be like that.
By convention just invented, acceleration is always in the +x direction of the ship (not the universe). Also by convention, the ship’s rotation of zero will be taken to have it pointing east, in the positive x direction.
- Aside
- I should note that given the movement of the ship on the screen yesterday, OPENRNDR seems to think that y increases downward on the screen. I’m hoping that that will not be a concern, but I won’t be surprised if it does. We’ll try to just work in x,y with wraparound and let the ships fall where they may.
Seriously, let’s get to it. Here’s the test. It won’t compile yet.
@Test
fun `acceleration works`() {
val control = Controls()
val ship = Ship(100.0, control)
assertThat(ship.realPosition).isEqualTo(Vector2.ZERO)
assertThat(ship.velocity).isEqualTo(Vector2.ZERO)
ship.update(1.0/60.0)
assertThat(ship.realPosition).isEqualTo(Vector2.ZERO)
assertThat(ship.velocity).isEqualTo(Vector2.ZERO)
control.accelerate = true
ship.update(1.0/60.0)
assertThat(ship.realPosition).isEqualTo(Vector2.UNIT_X)
assertThat(ship.velocity).isEqualTo(Vector2.UNIT_X)
}
This seems close to reasonable. The numbers need to be worked out, and sooner or later we’re going to get in trouble with floats. We’ll deal with that when it happens.
First, we need the Controls object.
I’m sure we’ll wind up here with an interface, but for now let’s just make the thing. Maybe keyboards and mouses will make them also. We’ll see what the code wants to be.
class Controls {
var accelerate = false
}
Naturally, IDEA helped by making the frame. I cleverly filled in the accelerate bit, though I bet IDEA would have helped with that as well.
Looks like the test will execute. I expect a failure on the final asserts. No, I’m mistaken: I need to update the Ship constructor.
class Ship(private val radius: Double, private val controls: Controls = Controls()) {
One thing I don’t like about Kotlin’s fear of nulls is that I wind up needing to default a lot of parameters in my objects. I could, of course, update the other few tests instead … Anyway now to test …
expected: Vector2(x=1.0, y=0.0)
but was: Vector2(x=0.0, y=0.0)
As I expected. I should add some descriptions to those. OK, done. The more important thing is to provide for the actual acceleration. For now at least, acceleration will be an inherent property of the ship. There’s no need to make it a parameter at this point.
My test seems to think that acceleration for 1/60th of a second is going to increase velocity by 1, so that suggests that acceleration is Vector2(60.0,0) …
class Ship(private val radius: Double, private val controls: Controls = Controls()) {
var realPosition: Vector2 = Vector2(0.0, 0.0)
var pointing: Double = 0.0
var velocity = Vector2(0.0, 0.0)
var acceleration = Vector2(60.0,0.0)
And in update …
fun update(deltaTime: Double) {
if (controls.accelerate) velocity += acceleration
val proposedPosition = realPosition + velocity*deltaTime
realPosition = cap(proposedPosition)
}
That seems like it might work. Test and see why not. LOL, tiny fool!
[velocity]
expected: Vector2(x=1.0, y=0.0)
but was: Vector2(x=60.0, y=0.0)
Might be prudent to scale it.
fun update(deltaTime: Double) {
if (controls.accelerate) velocity += acceleration*deltaTime
val proposedPosition = realPosition + velocity*deltaTime
realPosition = cap(proposedPosition)
}
Now that I am hip to the time issue, it’s clear that this test is still going to fail, because one tick’s worth of acceleration will give us a velocity of 1, but the position will only move by 1/60. Let’s test. Perfect:
[position]
expected: Vector2(x=1.0, y=0.0)
but was: Vector2(x=0.016666666666424135, y=0.0)
I think I need to learn how to test Doubles for “close enough”. I knew I was going to hate the use of Double sooner or later. After brief study, I need a helper function here.
@Test
fun `acceleration works`() {
val control = Controls()
val ship = Ship(100.0, control)
assertThat(ship.realPosition).isEqualTo(Vector2.ZERO)
assertThat(ship.velocity).isEqualTo(Vector2.ZERO)
ship.update(1.0/60.0)
assertThat(ship.realPosition).isEqualTo(Vector2.ZERO)
assertThat(ship.velocity).isEqualTo(Vector2.ZERO)
control.accelerate = true
ship.update(1.0/60.0)
checkVector(ship.velocity, Vector2.UNIT_X, "velocity")
checkVector(ship.realPosition, Vector2(1.0/60.0, 0.0), "position")
}
private fun checkVector(actual:Vector2, should: Vector2, description: String) {
assertThat(actual.x)
.describedAs("$description x")
.isEqualTo(should.x, within(0.0001))
assertThat(actual.y)
.describedAs("$description y")
.isEqualTo(should.y, within(0.0001))
}
My new checkVector
just accommodates the lack of a within
for asserting about Vector2. Probably I could write an extension but that’s not in my range of vision at the moment.
Let’s think clearly about the values we’re checking for.
Our nominal acceleration is 60 units per second in the x direction. So the acceleration in a period of 1/60 should be 1/60th of that, so velocity should increase by one. If velocity is one unit per second, then in 1/60th of a second, we should travel 1/60th of a unit.
I think that’s nearly good. And I’m really feeling the need for a constant here.
class ShipTest {
private val tick = 1.0/60.0
@Test
fun `Ship Happens`() {
val ship = Ship(100.0)
ship.velocity = Vector2(120.0,120.0)
ship.update(tick)
assertThat(ship.realPosition).isEqualTo(Vector2(2.0,2.0))
}
And so on. I think that acceleration in the x direction actually works. Commit: rudimentary acceleration. Breakfast is cooking, so let’s sum up.
Summary
I think in what we have so far we see the good and bad of testing with microtests in a Ship situation like this one. On the good side, we can see exactly what happens. That the test showed up my mistaken impression about the effect of acceleration is good. That they are a pain to write and get right, is not so good. I plan to carry on with the testing for now.
All in all, decent progress for a Sunday morning. See you next time!