Шаг 198.
Основы Kotlin.
Интерфейсы и абстрактные классы. Сражение в NyetHack

    На этом шаге мы рассмотрим использование изученных инструментов при реализации приложения.

    Для добавления боев в NyetHack нам придется использовать все наши знания об объектно-ориентированном программировании.

    В каждой комнате в NyetHack будет монстр, которого ваш герой должен победить наиболее жестоким способом из всех возможных: превращением его в null.

    Добавьте в класс Room свойство monster с типом Monster?, поддерживающим значение null, и инициализируйте его экземпляром Goblin. Обновите описание Room, чтобы игрок знал, есть ли в комнате монстр, готовый к сражению.

open class Room(val name: String) {
    protected open val dangerLevel = 5
    var monster: Monster? = Goblin()

    fun description() = "Room: $name\n" +
            "Danger level: $dangerLevel" +
            "Creature: ${monster?.description ?: "none."}"
    open fun load() = "Nothing much to see here..."
}

class TownSquare : Room("Town Square")  {
    override val dangerLevel = super.dangerLevel - 3

    override fun load() = "The villagers rally and cheer as you enter!"
}


Рис.1. Добавление монстра в каждую комнату (Room.kt)

    Если monster в Room имеет значение null, значит, его одолели. В противном случае герою еще предстоит сражение.

    Вы инициализировали monster, свойство типа Monster?, создав объект типа Goblin. Комната может содержать экземпляр любого подкласса Monster, а Goblin - подкласс Monster. Это пример полиморфизма. Если создать еще один класс, наследующий Monster, его также можно будет использовать в комнатах NyetHack.

    Теперь настало время добавить команду "сражение", чтобы воспользоваться новым свойством monster класса Room. Добавьте новую private-функцию с именем fight() в Game.

.   .   .   .
object Game {
.   .   .   .
    private fun move(directionInput: String) =
            try {
                val direction = Direction.valueOf(directionInput.toUpperCase())
                val newPosition = direction.updateCoordinate(player.currentPosition)
                if (!newPosition.isInBounds) {
                    throw IllegalStateException("$direction is out of bounds.")
                }
                val newRoom = worldMap[newPosition.y][newPosition.x]
                player.currentPosition = newPosition
                currentRoom = newRoom
                "OK, you move $direction to the ${newRoom.name}.\n${newRoom.load()}"
            } catch (e: Exception) {
                "Invalid direction: $directionInput."
            }

    private fun fight() = currentRoom.monster?.let {
        while (player.healthPoints > 0 && it.healthPoints > 0) {
            Thread.sleep(1000)
        }
        "Combat complete."
    } ?: "There's nothing here to fight."

}


Рис.2. Объявление функции fight() (Game.kt)

    Первое, что делает fight(), - проверяет значение monster. Если оно равно null, то сражаться не с кем, и тогда выводится соответствующее сообщение. Если монстр присутствует в комнате и у игрока и монстра остается хотя бы по одной жизни, проводится раунд сражения.

    Раунд сражения представлен private-функцией slay(), которую вы сейчас добавите. Функция slay() вызвает функцию attack() для монстра и игрока. Функцию attack() можно вызвать и для Player, и для Monster, потому что они оба реализуют интерфейс Fightable.

.   .   .   .
object Game {
.   .   .   .
    private fun fight() = currentRoom.monster?.let {
        while (player.healthPoints > 0 && it.healthPoints > 0) {
            Thread.sleep(1000)
        }
        "Combat complete."
    } ?: "There's nothing here to fight."

    private fun slay(monster: Monster) {
        println("${monster.name} did ${monster.attack(player)} damage!")
        println("${player.name} did ${player.attack(monster)} damage!")
        if (player.healthPoints <= 0) {
            println(">>>> You have been defeated! Thanks for playing. <<<<")
            exitProcess(0)
        }
        if (monster.healthPoints <= 0) {
            println(">>>> ${monster.name} has been defeated! <<<<")
            currentRoom.monster = null
        }
    }

}


Рис.3. Объявление функции slay() (Game.kt)

    Согласно условию цикла while в fight(), сражение длится до тех пор, пока у игрока или монстра не закончатся очки здоровья.

    Если healtPoints игрока достигнет 0, игра заканчивается и вызывается exitProcess(). exitProcess() - функция из стандартной библиотеки Kotlin, которая заканчивает работу текущего экземпляра JVM. Чтобы получить доступ к этой функции, нужно импортировать kotlin.system.exitProcess.

    Если healtPoints монстра достигнет 0, он будет обращен в null самым драматичным образом.

    Вызовите функцию slay() из fight().

.   .   .   .
object Game {
.   .   .   .
    private fun fight() = currentRoom.monster?.let {
        while (player.healthPoints > 0 && it.healthPoints > 0) {
            slay(it)
            Thread.sleep(1000)
        }
        "Combat complete."
    } ?: "There's nothing here to fight."

    private fun slay(monster: Monster) {
        println("${monster.name} did ${monster.attack(player)} damage!")
        println("${player.name} did ${player.attack(monster)} damage!")
        if (player.healthPoints <= 0) {
            println(">>>> You have been defeated! Thanks for playing. <<<<")
            exitProcess(0)
        }
        if (monster.healthPoints <= 0) {
            println(">>>> ${monster.name} has been defeated! <<<<")
            currentRoom.monster = null
        }
    }

}


Рис.4. Вызов функции slay() (Game.kt)

После раунда сражения вызывается Thread.sleep(). Thread.sleep() - мощная функция, которая откладывает выполнение на заданное время, в нашем случае - на 1000 миллисекунд (или на 1 секунду). Мы не рекомендуем использовать Thread.sleep() в окончательной версии кода, но в данном случае это удобный способ создать интервалы между раундами сражения.

    Как только условие цикла while перестает выполняться, функция вернет строку "Combat complete" для вывода в консоль.

    Испытайте новую систему сражения, добавив в GameInput() команду "fight", вызывающую функцию fight().

.   .   .   .
object Game {
    .   .   .   .
    private class GameInput(arg: String?) {
        private val input = arg ?: ""
        val command = input.commandsplit(" ")[0]
        val argument = input.split(" ").getOrElse(1, { "" })

        fun processCommand() = when (command.toLowerCase()) {
            "fight" -> fight()
            "move" -> move(argument)
            else -> commandNotFound()
        }

        private fun commandNotFound() = "I'm not quite sure what you're trying to do!"
    }
    .   .   .   .
}
Файл с проектом можно взять здесь.


Рис.5. Добавление команды "fight" (Game.kt)

    Запустите Game.kt. Попробуйте походить по комнатам и применить команду "fight" в разных комнатах. Элемент случайности, присутствующий в свойстве damageRoll интерфейса Fightable, будет создавать новую ситуацию в каждой комнате, в которой вы решите затеять драку.

Welcome, adventurer.
Room: Town Square
Danger level: 2
Creature: A nasty-looking goblin
The villagers rally and cheer as you enter!
The bell tower announces your arrival. GWONG
(Aura: GREEN) (Blessed: YES)
Madrigal of Sanorith is in excellent condition!
> Enter your command: fight
Goblin did 3 damage!
Madrigal of Sanorith did 28 damage!
Goblin did 10 damage!
Madrigal of Sanorith did 20 damage!
>>>> Goblin has been defeated! <<<<
Combat complete.
Room: Town Square
Danger level: 2
Creature: none.
The villagers rally and cheer as you enter!
The bell tower announces your arrival. GWONG
(Aura: GREEN) (Blessed: YES)
Madrigal of Sanorith has some minor wounds but is healing quite quickly!
> Enter your command: 

    В этих шагах вы использовали интерфейсы, чтобы определить, что необходимо для участия в сражении, а также абстрактные классы, чтобы создать базовый класс для всех монстров в NyetHack. Эти инструменты позволят вам построить отношения между классами, которые определяют, что может делать класс, а не как.

    Многие идеи объектно-ориентированного программирования, с которыми вы познакомились на предыдущих шагах, служат одной цели: использованию инструментов языка Kotlin для создания масштабируемого кода, который экспортирует только нужное и скрывает все остальное.

    В следующих шагах мы познакомимся с обобщениями - средствами языка, которые позволяют указывать классы, работающие со многими типами.

    Со следующего шага мы начнем знакомиться с обобщениями.




Предыдущий шаг Содержание Следующий шаг