На этом шаге мы рассмотрим использование изученных инструментов при реализации приложения.
Для добавления боев в 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 для создания масштабируемого кода, который экспортирует только нужное и скрывает все остальное.
В следующих шагах мы познакомимся с обобщениями - средствами языка, которые позволяют указывать классы, работающие со многими типами.
Со следующего шага мы начнем знакомиться с обобщениями.