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