Шаг 189.
Основы Kotlin.
Объекты. Исследуем мир NyetHack

    На этом шаге мы рассмотрим реализацию перемещения.

    Теперь, когда вы построили цикл игры и установили систему координат, настало время применить свои знания и добавить больше комнат для исследования в NyetHack.

    Для настройки карты мира вам понадобится список, включающий все комнаты. Более того, так как игрок может перемещаться в двух измерениях, вам понадобится список, содержащий внутри два других списка. Первый список комнат будет включать, с востока на запад, городскую площадь (начало игры), таверну и задний зал таверны. Второй список комнат будет включать коридор и просто комнату. Эти списки будут храниться в третьем списке с именем worldMap, представляющем координату у.

    Добавьте свойство worldMap в Game с набором комнат для исследования героем.

package com.bignerdranch.nyethack
import Player
import Room
import TownSquare

fun main() {
    Game.play()
}

object Game {
    private val player = Player("Madrigal")
    private var currentRoom: Room = TownSquare()

    private var worldMap = listOf(
            listOf(currentRoom, Room("Tavern"), Room("Back Room")),
            listOf(Room("Long Corridor"), Room("Generic Room")))

    init {
        println("Welcome, adventurer.")
    }
    .   .   .   .
}


Рис.1. Построение карты мира NyetHack (Game.kt)

    На рисунке 2 показана сетка комнат, которые можно посетить в NyetHack.


Рис.2. Карта мира NyetHack

    Теперь, когда комнаты на месте, настало время добавить команду для перемещения и дать игроку возможность перемещаться по миру. Добавьте функцию с именем move(), которая принимает направление движения как String. Многое происходит в move(); мы расскажем об этом после того, как вы напишете код.

.   .   .   .
object Game {
    private val player = Player("Madrigal")
    private var currentRoom: Room = TownSquare()

    private var worldMap = listOf(
            listOf(currentRoom, Room("Tavern"), Room("Back Room")),
            listOf(Room("Long Corridor"), Room("Generic Room")))

    init {
        println("Welcome, adventurer.")
    }
    .   .   .   .
    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."
            }
}


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

    Функция move() возвращает строку в зависимости от результата оператора try/catch. В блоке try мы вызываем функцию valueOf(), чтобы найти совпадение с вводом пользователя. Функция valueOf() доступна для всех классов перечислений и возвращает перечисляемый тип с именем, совпадающим с переданным строковым значением. Например, вызов Direction.valueOf("EAST") вернет Direction.EAST. Если передать значение, не совпадающее ни с одним из перечисляемых типов, будет возбуждено исключение IllegalArgumentException.

    Исключение будет обработано блоком catch (более того, так будет обработано любое исключение, возникшее в блоке try).

    Если вызов valueOf() выполнится успешно, тогда будет произведена проверка местоположения игрока в границах мира. Если он вышел за границы, будет возбуждено IllegalStateException, которое также обработает блок catch.

    Если игрок переместился в разрешенном направлении, то следующий шаг - запросить у worldMap комнату в новой позиции. Вы уже видели, как получать значения из коллекции по индексу, а тут вы просто повторяете это действие дважды. Первый индекс, worldMap[newPosition.y], возвращает список из списка с именем worldMap. Второй индекс, [newPosition.x], возвращает комнату внутри второго списка. Если комнаты с такими координатами нет, тогда возбуждается ArrayIndexOutOfBoundsException, и да, оно также обрабатывается блоком catch.

    Если весь код выполнится благополучно, тогда свойство игрока currentPosition обновится и в консоль будет отправлен текст, являющийся частью интерфейса NyetHack.

    Функция move() должна вызываться, только когда игрок вводит команду "move", что мы сейчас и реализуем, используя класс GameInput, написанный нами ранее.

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()) {
            "move" -> move(argument)
            else -> commandNotFound()
        }

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


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

    Запустите Game.kt и попробуйте переместиться. Вы увидите следующее:

Welcome, adventurer.
Room: Town Square
Danger level: 2
The villagers rally and cheer as you enter!
The bell tower announces your arrival. GWONG
(Aura: GREEN) (Blessed: YES)
Madrigal of Abelhaven is in excellent condition!
> Enter your command: move east
OK, you move EAST to the Tavern.
Nothing much to see here...
Room: Tavern
Danger level: 5
Nothing much to see here...
(Aura: GREEN) (Blessed: YES)
Madrigal of Abelhaven is in excellent condition!
> Enter your command: 


Рис.5. Результат выполнения программы

    Вот и все. Теперь можно перемещаться по миру NyetHack. В этих шагах вы научились пользоваться разными видами классов. Кроме ключевого слова class, для представления данных вы можете использовать объявления объектов (синглтоны), классы данных и перечисления. Использование правильных инструментов делает отношения между объектами более прямолинейными.

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

    На следующем шаге мы рассмотрим объявление структурного сравнения.




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