GitHub Repo

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.

  1. Game refers to SolidOjbect.ship.
  2. Thirty tests refer to SolidObject.ship.
  3. 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()
        if (splitCount >= 1) {
        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.


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)

    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
        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,