Kotlin 7
So few lines, so many design questions. We’ll work more on Ship motion. P.S. Learned a thing, see the very end.
From the viewpoint of what the Ship needs, I see at least these things:
Ship knows its current location;(Done)Ship knows its current velocity;(Done)Ship steps according to velocity;(Done)- Ship wraps around in toroidal space (0,0)..(xMax,yMax)
- Ship accelerates along its forward axis;
- Ship enforces a maximum speed;
- Ship can rotate; (added, see below)
There is also a design issue around connecting the Ship, which is an object that knows and does those things, to the display, which shows us some kind of picture of what’s going on. I am of course thinking of a flat display à la Asteroids (Bucketoids?), but in principle I suppose the display could be the view out the expansive windshield of the ship, or a radar display showing what is around the ship, oriented to forward direction. Those would make for very different game feel.
I’ve kept the Ship independent of display so far, and have yet to figure out a good way to connect it to the display. And I am hopeful, if not optimistic, that my colleagues will be able to help me get converted to libGDX rather than TornadoFX, if that’s the thing to do. In any case, display is later.
A more immediate design concern is bugging me. Yesterday I refactored (and implemented) from a mode where the ship knew its x and y and dx and dy, to a mode where those are encapsulated in Coordinates and Velocity objects, and, most concerning, every time the ship moves, a instance of Velocity is created and destroyed, and a new instance of Coordinates is created and an older one destroyed.
I did this in aid of making Coordinates and Velocity immutable, which is generally thought to be a good way to be for things that are essentially numbers. However, we’ll be creating and destroying objects at the rate off 120 per second, 7200 per minute, 43,200 per hour, 2,332,800 per day, almost a billion objects per year, year in and year out. Over the life of the universe, this could add up.
The wastage, the unnecessary deaths of all those little Coordinates and Velocities, it weighs on me. I am concerned that there should, perhaps, not be such a so callous disregard for those tiny ephemeral objects.
I admit this isn’t bothering me a lot, but in all seriousness it might be an efficiency consideration.
I was taught not to optimize unless and until a performance measurement showed the optimization was needed. But I was also taught not to do stupid things, although the latter lesson seems not to have taken quite as well as one might have hoped. For now, balancing the many forces acting upon me, I’ll leave the situation as it is.
But it bugs me, and I think about it. I have been told that thinking is good, if you don’t let it run away with you.
As I was serving the cat just now, since she demanded food, I was thinking that it may seem like a lot of thinking for about 40 or 50 lines of code, and that if one kept thinking that much one would never get anything done.
I can assure you that if you write seven articles about 50 lines of code, you may have trouble getting things done, but the amount of actually thinking about the design that I do is probably pretty close to right. Or, if not right, it’s the best balance I can find.
It’s sharing my thinking with you that takes the time. So let’s stop doing that and deal with our next story.
Wrapping, vMax, or Acceleration?
So which story? We can’t really do vMax before acceleration, so that’s right out. Acceleration will be essentially a method inside of the Ship class, so that is easy. Wrapping, however, is a property of the universe, and we don’t have a universe yet.
I’m not sure what the Universe should be. I think that, like our own, it is probably made of mathematics, and mostly, in our case, interesting constants like its width and height. I think, for now, the universe can be a dictionary (Kotlin: map) containing named constants. I think it’ll be a nap from string (the constant name) to Double, at least for now. We may find that Kotlin’s insistence on knowing what everyone is will get in our way.
I’ll try the map idea, and therefore, let’s do the wrapping first.
OK, Wrapping
Here’s our code for moving the Ship:
fun move(timeMS: Double) {
val scale = timeMS/1000.0
val scaled = velocity*scale
val newPlace = Coordinates(x + scaled.dx, y + scaled.dy)
coords = newPlace
}
We need to limit newPlace
so that x and y are always non-negative (the universe starts at 0,0), and do not exceed Universe.xMax and Universe.yMax respectively.
What is the definition of a negative number modulo a positive number? Reading the definition I’m not sure. We have ways: we’ll write a test.
class GeneralTests {
@Test
fun modulo() {
val high = 4000.0
val low = 0.0
assertThat(5.0%high).isEqualTo(5.0)
assertThat(4005.0%high).isEqualTo(5.0)
assertThat(-5.0%4000).isEqualTo(-5.0)
}
}
OK mod on a negative is negative. I think I want a function that just does what I need. I see that Kotlin has a function coerceIn
but it just keeps things in the range, so that if you tried to run over the edge you’d just stay on the edge. Not what I have in mind. I think I just discovered that the Universe may be a class.
Anyway, let’s TDD a function that does what we want.
I’ll write a new test in my GeneralTests
that says what I want:
@Test
fun checkWrap() {
assertThat(wrap(0.0, 4005.0, 4000.0)).isEqualTo(5.0)
}
That’s a start. function signature to be defined in a moment … IDEA wants to help … but not to do what I need. I wind up with this test, after tidying:
@Test
fun checkWrap() {
val low = 0.0
val high = 4000.0
assertThat(wrap(low, 4005.0, high)).isEqualTo(5.0)
assertThat(wrap(low, 5.0, high)).isEqualTo((5.0))
assertThat(wrap(low, 5.0, high)).isEqualTo((5.0))
}
And the function:
fun wrap(low: Double,actual: Double,high: Double): Double {
return if (actual > high) actual%high
else if (actual < low) high - actual%high
else actual
}
IDEA did suggest moving return to the beginning of that one. I had written it with return
after each if / else. Thanks IDEA.
So that works. But as so often happens, there’s something to think about here.
Think, Pooh, Think!
I think we agree that there must be some kind of Universe that holds onto constants and such. It also seems obvious, when you look around you at the one we live in, that Universe also defines the geometry of, well, the universe. One might say that the Universe is the geometry.
Let’s rename this test class and focus it on testing the Universe. A big job, but someone’s got to do it.
I have read that Kotlin has a thing like a class that is just an object. An instance. That seems, maybe, like just the thing. (I am surely wrong about this. Won’t we have universes that are specific to the game machine?) A bit of research into object
tells me that either it isn’t ready for me, or I’m not ready for it. We’ll create a Universe class.
Let me revise the test …
Darn it, I forgot to commit. Let’s do that.
I’m glad I did that. The commit pointed out that the value low
is never used, since we are assuming the universe starts at zero. I’m not sure what it’s complaining about. Oh, in one of the tests. OK. Remove it commit again.
Darn, that sort of interruption derails my thinking. Valuable, I suppose, but distracting. Maybe I’ll get used to it. Anyway I want to rename these tests to UniverseTests
.
I was thinking I’d edit the test on wrap. but that seems awkward and I might as well show the function there, at least for now. Let’s see … what really wants to happen in the universe is that if you generate a coordinate that is outside the range, it is wrapped back in. Let’s write a test for that:
@Test
fun universeWraps() {
val u = Universe(xMax=4000.0, yMax=3000.0)
val c = Coordinates(x=-5.0,y=3005.0)
val expected = Coordinates( x=3995.0, y=5.0)
val w: Coordinates = u.wrap(c)
assertThat(w).isEqualTo(expected)
}
I think that’s what I want. IDEA objects to the lack of a Universe. Who wouldn’t, really?
I found that Coordinates don’t print well, and I’m not sure how to give them a decent print. For now, I change the test to this:
@Test
fun universeWraps() {
val u = Universe(xMax=4000.0, yMax=3000.0)
val c = Coordinates(x=-5.0,y=3005.0)
val expected = Coordinates( x=3995.0, y=5.0)
val w: Coordinates = u.wrap(c)
assertThat(w.x).isEqualTo(expected.x)
assertThat(w.y).isEqualTo(expected.y)
}
And here’s my implementation of Universe, with wrap:
class Universe(val xMax: Double, val yMax: Double) {
fun wrap(low: Double,actual: Double,high: Double): Double {
return if (actual > high) actual%high
else if (actual < low) high - actual%high
else actual
}
fun wrap(c:Coordinates): Coordinates {
val wx = wrap(0.0, c.x, xMax)
val wy = wrap(0.0, c.y, yMax)
return Coordinates(wx,wy)
}
}
Note that I have two implementations of wrap, one that works on doubles and one for Coordinates. And I thought that it would be correct, but here’s what happens:
expected: 3995.0
but was: 4005.0
I see the bug but where is my test for that in the original wrap function.
@Test
fun checkWrap() {
val low = 0.0
val high = 4000.0
assertThat(wrap(low, 4005.0, high)).isEqualTo(5.0)
assertThat(wrap(low, 5.0, high)).isEqualTo((5.0))
assertThat(wrap(low, 5.0, high)).isEqualTo((5.0))
}
Ah. That last test should be on -5.0. Change it and expect two errors of the same kind.
@Test
fun checkWrap() {
val low = 0.0
val high = 4000.0
assertThat(wrap(low, 4005.0, high)).isEqualTo(5.0)
assertThat(wrap(low, 5.0, high)).isEqualTo((5.0))
assertThat(wrap(low, -5.0, high)).isEqualTo((3995.0))
}
Yes, now I get two failures. The fix is the same for each … I am already getting a negative value back from the mod, so …
fun wrap(low: Double,actual: Double,high: Double): Double {
return if (actual > high) actual%high
else if (actual < low) high + actual%high // mod is negative
else actual
}
This should fix the one … and it does. However, the second is going to fail again. First make the change there:
class Universe(val xMax: Double, val yMax: Double) {
fun wrap(low: Double,actual: Double,high: Double): Double {
return if (actual > high) actual%high
else if (actual < low) high + actual%high // mod is negative
else actual
}
Ah. It works. I was thinking I had checked the high y against 3995, but it was the x. Here’s the test, which now runs:
@Test
fun universeWraps() {
val u = Universe(xMax=4000.0, yMax=3000.0)
val c = Coordinates(x=-5.0,y=3005.0)
val expected = Coordinates( x=3995.0, y=5.0)
val w: Coordinates = u.wrap(c)
assertThat(w.x).isEqualTo(expected.x)
assertThat(w.y).isEqualTo(expected.y)
}
I think we’d better extend that test to check the other ends:
@Test
fun universeWraps() {
val u = Universe(xMax=4000.0, yMax=3000.0)
var c = Coordinates(x=-5.0,y=3005.0)
var expected = Coordinates( x=3995.0, y=5.0)
var w: Coordinates = u.wrap(c)
assertThat(w.x).isEqualTo(expected.x)
assertThat(w.y).isEqualTo(expected.y)
c = Coordinates(4006.0, -7.0)
w = u.wrap(c)
expected = Coordinates(6.0, 2993.0)
assertThat(w.x).isEqualTo(expected.x)
assertThat(w.y).isEqualTo(expected.y)
// assertThat(w).isEqualTo(expected)
}
That runs green, but if I try to assert on the whole coordinate, they are not equal. How do I define equality on my objects?
This seems to do the job:
class Coordinates(val x: Double, val y: Double){
override operator fun equals(other: Any?): Boolean {
when (other) {
is Coordinates -> return x==other.x && y==other.y
else -> return false
}
}
}
Now this test is green:
@Test
fun universeWraps() {
val u = Universe(xMax=4000.0, yMax=3000.0)
var c = Coordinates(x=-5.0,y=3005.0)
var expected = Coordinates( x=3995.0, y=5.0)
var w: Coordinates = u.wrap(c)
assertThat(w.x).isEqualTo(expected.x)
assertThat(w.y).isEqualTo(expected.y)
c = Coordinates(4006.0, -7.0)
w = u.wrap(c)
expected = Coordinates(6.0, 2993.0)
assertThat(w.x).isEqualTo(expected.x)
assertThat(w.y).isEqualTo(expected.y)
assertThat(w).isEqualTo(expected)
}
And I can commit, but I have a warning …
Warning:(3, 7) Class has 'equals()' defined but does not define 'hashCode()'
I think for now, I’ll back off from equals in Coordinates. I’l just comment those bits out.
Commit: Universe has size and can wrap.
Let’s reflect.
Reflection
I’ve been at this for 2 1/2 hours, gained another couple of lines of code. Should I feel glad or sad? No. I should feel relaxed or tense (relaxed). I should feel more knowledgeable or more confused (more knowledgeable). I should feel interested in doing more, or not interested (interested).
I have no reason to expect any particular pace of implementation, and I’m bloody well not looking for some spawny git to come in here and tell me how fast I should be going. We do not have an opening here chez Ron for such an individual.
Today so far I’ve made a tiny but, I think, good step: there is a Universe class, and it knows how to wrap coordinates to within itself. the universe is toroidal and can have any reasonable xMax and yMax. It does not support negative coordinates. The universe is two-dimensional.
I guess the next thing is acceleration. The Ship can only accelerate in the direction it is facing, since its engine points out the back. It can rotate by some unspecified means. So to move in a different direction, you rotate and then accelerate (or decelerate), which will cause you to curve in space. This reminds me to go up and add an item to the list of features at the top, in case I use it in the future:
- Ship can rotate;
All the things on the list seem pretty straightforward to express in the Kotlin subset that I know.
I’ve learned a few neat things along the way. Let’s see, learnings include the basics, plus:
- General use of IDEA and Kotlin
- A few Kotlinisms like return if/else
- Defining functions with same name, variable args
- (Probably doing that where it’s not ideal)
- Creating tests in IDEA/Kotlin
- Using some refactorings, including rename and others
- How to add
equals
(but nothashCode
) - Defining operators like Coordinates+Velocity
- Committing code using IDEA git feature
- Object “properties” that forward to member variables
I’m feeling more comfortable with IDEA and Kotlin. Getting used to the strictness, and not feeling so blindsided by it. I think I’m developing toward a different order of doing things, but soon I’ll probably be oblivious to this, and just be doing thing in an order that seems to make sense.
I am still somewhat irritated by the difference between not compiling and the tests not running. Oddly the same thing doesn’t bother me in Lua, something about when and where one gets the message.
I am quite surprised to find how slow my setup is in getting to the tests. My iPad gets to the tests faster in Codea, with a lot more code than I’ve presently got in this little app. (Yes, I know, there’s stuff going on behind the scenes … but there is in Lua as well.) It’s only seconds, but there’s just a lot of foofaraw going on.
Anyway, I think we’re done for the morning, and we’ll do more tomorrow, unless I get so excited that I do something this afternoon.
See you then, whenever then is!
P.S. Learned about data classes. This class definition:
data class Coordinates(val x: Double, val y: Double)
Allows this test to pass:
@Test
fun universeWraps() {
val u = Universe(xMax=4000.0, yMax=3000.0)
var c = Coordinates(x=-5.0,y=3005.0)
var expected = Coordinates( x=3995.0, y=5.0)
var w: Coordinates = u.wrap(c)
assertThat(w.x).isEqualTo(expected.x)
assertThat(w.y).isEqualTo(expected.y)
c = Coordinates(4006.0, -7.0)
w = u.wrap(c)
expected = Coordinates(6.0, 2993.0)
assertThat(w.x).isEqualTo(expected.x)
assertThat(w.y).isEqualTo(expected.y)
assertThat(w).isEqualTo(expected)
}
Including that final isEqualTo
.
Arguably, I should perhaps have some Coordinate tests, but it has no behavior other than the newly automatically provided ==
and hashCode
.
Live and learn!