Refactoring NyetHack

You have learned about class functions, properties, and encapsulation, and you have done some of the work to apply these concepts to NyetHack. It is time to finish the job and thoroughly clean up NyetHack’s code.

You will be moving chunks of code from one file to another. It helps to see the two files side by side. Fortunately, IntelliJ provides this feature.

With Game.kt open, right-click on the Player.kt tab at the top of the editor and select Split Vertically (Figure 12.3).

Figure 12.3  Splitting the editor vertically

Splitting the editor vertically

You now have another editor pane to work in (Figure 12.4). (You can drag tabs between editor panes to configure your editor experience to your liking.)

Figure 12.4  Two panes

Two panes

This is a complex refactor, but by the end of this section Player will expose a selective API and encapsulate the implementation details that other components do not need to know about. In short: It is for a good cause.

First, locate the variables declared in Game.kt’s main function that make sense as properties of Player. These include healthPoints, isBlessed, and isImmortal. Refactor them to become properties of Player.

Listing 12.15  Removing variables from main (Game.kt)

fun main(args: Array<String>) {
    var healthPoints = 89
    val isBlessed = true
    val isImmortal = false

    val player = Player()
    player.castFireball()
    ...
}
...

As you add them to Player.kt, be sure that the variables are all defined inside the Player class’s body.

Listing 12.16  Adding properties to Player (Player.kt)

class Player {
    var name = "madrigal"
        get() = field.capitalize()
        private set(value) {
            field = value.trim()
        }

    var healthPoints = 89
    val isBlessed = true
    val isImmortal = false

    fun castFireball(numFireballs: Int = 2) =
            println("A glass of Fireball springs into existence. (x$numFireballs)")
}

These changes will result in a number of errors in Game.kt. Hang tight; by the time you are finished, all the errors will be taken care of.

healthPoints and isBlessed will be accessed from Game.kt. But isImmortal is never accessed from outside of Player, so it behooves you to make isImmortal private. Encapsulate the property by making it private to ensure that other classes will not have access to it.

Listing 12.17  Encapsulating isImmortal within Player (Player.kt)

class Player {
    var name = "madrigal"
        get() = field.capitalize()
        private set(value) {
            field = value.trim()
        }

    var healthPoints = 89
    val isBlessed = true
    private val isImmortal = false

    fun castFireball(numFireballs: Int = 2) =
            println("A glass of Fireball springs into existence. (x$numFireballs)")
}

Next, review the functions declared in Game.kt. printPlayerStatus prints out the textual interface for the game, so it is appropriate for it to be declared in Game.kt. But auraColor and formatHealthStatus both relate directly to the player, rather than the gameplay. Therefore, those two functions belong in the class definition rather than in main.

Move auraColor and formatHealthStatus into Player.

Listing 12.18  Removing functions from main (Game.kt)

fun main(args: Array<String>) {
    ...
}

private fun formatHealthStatus(healthPoints: Int, isBlessed: Boolean) =
        when (healthPoints) {
            100 -> "is in excellent condition!"
            in 90..99 -> "has a few scratches."
            in 75..89 -> if (isBlessed) {
                "has some minor wounds, but is healing quite quickly!"
            } else {
                "has some minor wounds."
            }
            in 15..74 -> "looks pretty hurt."
            else -> "is in awful condition!"
        }

private fun printPlayerStatus(auraColor: String,
                              isBlessed: Boolean,
                              name: String,
                              healthStatus: String) {
    println("(Aura: $auraColor) " +
            "(Blessed: ${if (isBlessed) "YES" else "NO"})")
    println("$name $healthStatus")
}

private fun auraColor(isBlessed: Boolean,
                      healthPoints: Int,
                      isImmortal: Boolean): String {
    val auraVisible = isBlessed && healthPoints > 50 || isImmortal
    val auraColor = if (auraVisible) "GREEN" else "NONE"
    return auraColor
}

Again, make sure the refactored functions are inside the class’s body.

Listing 12.19  Adding class functions to Player (Player.kt)

class Player {
    var name = "madrigal"
        get() = field.capitalize()
        private set(value) {
            field = value.trim()
        }

    var healthPoints = 89
    val isBlessed = true
    private val isImmortal = false

    private fun auraColor(isBlessed: Boolean,
                          healthPoints: Int,
                          isImmortal: Boolean): String {
        val auraVisible = isBlessed && healthPoints > 50 || isImmortal
        val auraColor = if (auraVisible) "GREEN" else "NONE"
        return auraColor
    }

    private fun formatHealthStatus(healthPoints: Int, isBlessed: Boolean) =
            when (healthPoints) {
                100 -> "is in excellent condition!"
                in 90..99 -> "has a few scratches."
                in 75..89 -> if (isBlessed) {
                    "has some minor wounds, but is healing quite quickly!"
                } else {
                    "has some minor wounds."
                }
                in 15..74 -> "looks pretty hurt."
                else -> "is in awful condition!"
            }

    fun castFireball(numFireballs: Int = 2) =
            println("A glass of Fireball springs into existence. (x$numFireballs)")
}

That takes care of the cutting and pasting, but there is work left to do in both Game.kt and Player.kt. For now, turn your attention to Player.

(If you split your editor earlier, you can un-split it now by closing all the files open in a pane. Close files by clicking the X in their tab [Figure 12.5] or by pressing Command-W [Ctrl-W].)

Figure 12.5  Closing a tab in IntelliJ

Closing a tab in IntelliJ

In Player.kt, notice that the functions previously declared in Game.kt that were moved to PlayerauraColor and formatHealthStatus – take in values that are now properties of PlayerisBlessed, healthPoints, and isImmortal. When the functions were defined in Game.kt, they were outside of Player’s class scope. But because they are now class functions on Player, they have access to all of the properties declared in Player.

This means that the class functions in Player no longer need any of their parameters, as they can all be accessed from within the Player class.

Modify the function headers to remove their parameters.

Listing 12.20  Removing unnecessary parameters from class functions (Player.kt)

class Player {
    var name = "madrigal"
        get() = field.capitalize()
        private set(value) {
            field = value.trim()
        }

    var healthPoints = 89
    val isBlessed = true
    private val isImmortal = false

    private fun auraColor(isBlessed: Boolean,
                          healthPoints: Int,
                          isImmortal: Boolean): String {
        val auraVisible = isBlessed && healthPoints > 50 || isImmortal
        val auraColor = if (auraVisible) "GREEN" else "NONE"
        return auraColor
    }

    private fun formatHealthStatus(healthPoints: Int, isBlessed: Boolean) =
            when (healthPoints) {
                100 -> "is in excellent condition!"
                in 90..99 -> "has a few scratches."
                in 75..89 -> if (isBlessed) {
                    "has some minor wounds, but is healing quite quickly!"
                } else {
                    "has some minor wounds."
                }
                in 15..74 -> "looks pretty hurt."
                else -> "is in awful condition!"
            }

    fun castFireball(numFireballs: Int = 2) =
            println("A glass of Fireball springs into existence. (x$numFireballs)")
}

Before this change, a reference to healthPoints within the formatHealthStatus function would be a reference to formatHealthStatus’s parameter, because that reference was scoped to the function. Without a variable named healthPoints within the function scope, the next most local scope is at the class level, where the healthPoints property is defined.

Next, notice that the two class functions are defined as private. This was not a problem when they were defined in the same file from which they were accessed. But now that they are private to the Player class, they are not visible to other classes. These functions should not be encapsulated, so make them visible by removing the private keyword from auraColor and formatHealthStatus.

Listing 12.21  Making class functions public (Player.kt)

class Player {
    var name = "madrigal"
        get() = field.capitalize()
        private set(value) {
            field = value.trim()
        }

    var healthPoints = 89
    val isBlessed = true
    private val isImmortal = false

    private fun auraColor(): String {
        ...
    }

    private fun formatHealthStatus() = when (healthPoints) {
        ...
    }

    fun castFireball(numFireballs: Int = 2) =
            println("A glass of Fireball springs into existence. (x$numFireballs)")
}

At this point, your properties and functions are declared in the correct places, but their invocation syntax in Game.kt is no longer correct, for three reasons:

  1. printPlayerStatus no longer has access to the variables that it needs to do its job, because those variables are now properties of Player.

  2. Now that functions like auraColor are class functions declared in Player, they need to be called on an instance of Player.

  3. Player’s class functions need to be called with their new, parameterless signatures.

Refactor printPlayerStatus to take a Player as an argument that can be used to access any properties necessary and to call the new, parameterless versions of auraColor and formatHealthStatus.

Listing 12.22  Calling class functions (Game.kt)

fun main(args: Array<String>) {
    val player = Player()
    player.castFireball()

    // Aura
    val auraColor = player.auraColor(isBlessed, healthPoints, isImmortal)

    // Player status
    val healthStatus = formatHealthStatus(healthPoints, isBlessed)
    printPlayerStatus(playerauraColor, isBlessed, player.name, healthStatus)

    // Aura
    player.auraColor(isBlessed, healthPoints, isImmortal)
}

private fun printPlayerStatus(player: PlayerauraColor: String,
                              isBlessed: Boolean,
                              name: String,
                              healthStatus: String) {
    println("(Aura: ${player.auraColor()}) " +
            "(Blessed: ${if (player.isBlessed) "YES" else "NO"})")
    println("${player.name} ${player.formatHealthStatus()}")
}

This change to printPlayerStatus’s header keeps it clean from the implementation details of Player. Compare these two signatures:

    printPlayerStatus(player: Player)
    printPlayerStatus(auraColor: String,
                      isBlessed: Boolean,
                      name: String,
                      healthStatus: String)

Which is cleaner to call? The latter requires the caller to know quite a lot about the implementation details of Player. The former simply requires an instance of Player. Here you see one of the benefits of object-oriented programming: Since the data is now a part of the Player class, it can be referenced without having to explicitly pass it to and from each function.

Take a step back and assess what you have accomplished in this refactor. The Player class now owns all the data and behaviors specific to a player entity in the game. It deliberately exposes three properties and three functions and encapsulates all other implementation details as private concerns that only the Player class should have access to. These functions advertise capabilities of the player: The player can provide a health status, the player can tell you their aura color, etc.

As your applications grow in scale, keeping scope manageable is imperative. By embracing object-oriented programming, you subscribe to the idea that each object should hold its own responsibilities and expose only the properties and functions that other functions and classes should see. Now, Player exposes what it means to be a player of NyetHack, and Game.kt holds the game loop in a much more readable main function.

Run Game.kt to confirm that everything works as it did before. And pat yourself on the back for completing that refactor. In the chapters to come, you will build on this solid foundation for NyetHack, adding complexity and features that rely on the object-oriented programming paradigm.

In the next chapter, you will add more ways to instantiate Player as you learn about initialization. But before growing your application further, it is a good time to learn about packages.