Kotlin 158: Ship it!
I think I’m ready to create a Ship class and remove SolidObject (or turn it into ship, whichever works. Let’s do this!
There are some users of the SolidObject
class. Let’s see what they are up to. They are all tests, can probably refer to something else.
- Game refers to SolidOjbect.ship.
- Thirty tests refer to SolidObject.ship.
- Twenty-odd other places refer to SolidObject one way or another.
I’m going to try something. I think there are no objects subclassing from SolidObject. I believe I can test that by closing it. We just remove open
from its definition and try to run. Tests all run.
Let’s rename SolidObject to Ship. What harm could it do?
Tests still run, but this can’t be sufficient, there are all those interactWithSolidObject
calls out there. I have to try the game. Game works. Let’s find those interactions.
Kotlin has plugged in the ship in the interaction definition already:
val interactWithSolidObject: (solid: Ship, trans: Transaction) -> Unit = { _, _, -> },
Let’s change the signature and then rename.
val interactWithShip: (ship: Ship, trans: Transaction) -> Unit = { _, _, -> },
We’re good. Commit: Renamed SolidObject to Ship throughout.
We have some warnings, regarding the finalizers. Let’s move them into the class they are associated with. In Asteroid:
override fun finalize(): List<ISpaceObject> {
return finalizer.finalize(this)
}
Let’s just copy-paste:
override fun finalize(): List<ISpaceObject> {
val objectsToAdd: MutableList<ISpaceObject> = mutableListOf()
val score = getScore()
objectsToAdd.add(score)
if (splitCount >= 1) {
objectsToAdd.add(asSplit(this))
objectsToAdd.add(asSplit(this))
}
return objectsToAdd
}
private fun asSplit(asteroid: Asteroid): Asteroid {
val newKr = asteroid.killRadius / 2.0
val newVel = asteroid.velocity.rotate(Math.random() * 360.0)
return Asteroid(
position = asteroid.position,
velocity = newVel,
killRadius = newKr,
splitCount = splitCount - 1
)
}
private fun getScore(): Score {
val score = when (splitCount) {
2 -> 20
1 -> 50
0 -> 100
else -> 0
}
return Score(score)
}
A bit of judicious editing and we are green. Remove the Finalizer. Remove the one we created in the Asteroid, and fix up a test of asteroid finalization. Green. Commit: remove class AsteroidFinalizer.
Let’s cruise the Ship code and see what we can improve.
The scale function is no longer used. Green. Remove scale from interface.
Accelerate is used only in Ship, remove from the interface. Green, commit.
isAsteroid
is unused. Safe delete. Some kind of fail, did it manually. Green, commit.
Controls can be removed from the interface but Ship still needs it in constructor. Green, commit.
View moves into class, removed from interface. Green, commit.
I plan to show you the final result but here’s an interim report:
interface ISolidObject : ISpaceObject {
var position: Point
var velocity: Velocity
var killRadius: Double
val finalizer: IFinalizer
var heading: Double
fun deathDueToCollision(): Boolean
override fun draw(drawer: Drawer)
override fun finalize(): List<ISpaceObject>
fun move(deltaTime: Double)
override fun toString(): String
fun turnBy(degrees: Double)
override fun beforeInteractions()
override fun afterInteractions(trans: Transaction)
override fun update(deltaTime: Double, trans: Transaction)
}
class Ship(
override var position: Point,
override var velocity: Velocity,
override var killRadius: Double = -Double.MAX_VALUE,
val controls: Controls = Controls(),
override val finalizer: IFinalizer = DefaultFinalizer()
)
The turnBy can be taken inside. Commit. Same with move. Let’s safe delete toString. I think it got put in there by IDEA anyway. Idea doesn’t want to take it out. Just delete it from the interface. Green. Commit.
deathDueToCollision
is only in Ship, remove from the interface.
Finally I notice that the only implementor of ISolidObject is Ship. Can’t I just remove the whole thing, base Ship in ISpaceObject like everyone else? Yes. Test. Commit: Remove ISolidObject interface entirely.
Whee, this is fun. Move killRadius inside, remove from constructor. Breaks a test. Put it back.
One more, how about finalizer? Yes, move it in, remove from companion. Green, commit.
I’ll append Ship as it stands now. Still fairly long but nothing in there that it doesn’t need. Well, not at this glance. We can probably do better, and we should remove the companion, but there are about 30 tests relying on it.
Summary
This has finished up quite nicely. There’s more to be done, and fresh eyes are needed. This was just an easy evening of knocking things off the table. It was a delight to discover that with all the other objects disconnected, renaming SolidObject to Ship did most of the work.
And IDEA helped a lot. I would not want to have had to change all those references manually. refactoring tools FTW.
See you next time!
class Ship(
var position: Point,
var velocity: Velocity,
var killRadius: Double = -Double.MAX_VALUE,
val controls: Controls = Controls(),
) : ISpaceObject, InteractingSpaceObject {
var heading: Double = 0.0
val view = ShipView()
val finalizer = ShipFinalizer()
override fun update(deltaTime: Double, trans: Transaction) {
controls.control(this, deltaTime, trans)
move(deltaTime)
}
fun accelerate(deltaV: Acceleration) {
velocity = (velocity + deltaV).limitedToLightSpeed()
}
fun deathDueToCollision(): Boolean {
return !controls.recentHyperspace
}
override fun draw(drawer: Drawer) {
drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
drawer.translate(position)
view.draw(this, drawer)
}
private fun weAreInRange(asteroid: Asteroid): Boolean {
return position.distanceTo(asteroid.position) < killRadius + asteroid.killRadius
}
override fun finalize(): List<ISpaceObject> {
return finalizer.finalize(this)
}
fun move(deltaTime: Double) {
position = (position + velocity * deltaTime).cap()
}
override val interactions: Interactions = Interactions(
interactWithAsteroid = { asteroid, trans ->
if (weAreInRange(asteroid)) trans.remove(this) },
interactWithShipDestroyer = { _, trans ->
if (this.isShip()) trans.remove(this)}
)
private fun isShip(): Boolean = this.killRadius == 150.0
override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
other.interactions.interactWithShip(this, trans)
}
override fun toString(): String {
return "Ship $position ($killRadius)"
}
fun turnBy(degrees: Double) {
heading += degrees
}
override fun beforeInteractions() {}
override fun afterInteractions(trans: Transaction) {}
companion object {
fun ship(pos: Point, control: Controls = Controls()): Ship {
return Ship(
position = pos,
velocity = Velocity.ZERO,
killRadius = 150.0,
controls = control,
)
}
}
}