Я не особо геймер. Мои родители больше относились к старой школе, читающей книги, поэтому много лет у нас даже не было телевизора, не говоря уже об игровой приставке. Но у нас был компьютер с Microsoft DOS 2.1, и этого было достаточно, чтобы запустить пару интерактивных текстовых приключенческих игр, включая Zork и Adventure, the Colossal Cave. Я любил эти игры. Лучше меньше, да лучше: мое воображение было ярче, чем графика любого графического процессора.

Я подумал, что было бы интересно создать одну из этих игр с использованием Elixir, отчасти из-за ностальгии, но также потому, что он предлагает полезную предпосылку для обсуждения того, как обрабатывать состояние в приложении.

Ингредиенты для текстового приключения

  • Способ управления состоянием игры (чтобы игрок знал свое местоположение, инвентарь, счет и т. Д.).
  • Способ легко создавать и управлять описаниями локаций в игре.
  • Способ обработки ввода / вывода текста.
  • странное, не описательное имя, которое пытается заманить игроков своей загадочностью, напоминающей ботаников. Xventure? Идеально.

В конечном итоге мы собираемся сделать что-то похожее на это:

Я обратился к YAML как к простому способу хранения описаний различных локаций в игре и к yaml_elixir как к пакету Elixir для их декодирования. Я ел свой собственный корм для собак и использовал созданные мной пакеты, чтобы помочь в этой работе: cowrie для работы с вводом и выводом CLI и figlet для генерации текста ASCII. Но я не хочу тратить время зря на тривиальные подробности об избавлении от велосипеда. Что я действительно хотел изучить, так это как управлять состоянием.

Возможно, вы где-то слышали, что процессы - это единственный способ поддерживать состояние в Elixir. Технически это может быть правдой, но это неточно или, по крайней мере, слишком упрощенно. В глубине души я думал, что обращусь к GenServer, чтобы управлять состоянием игры. Но когда я собрал это доказательство концепции, я понял, что мне вообще не нужны GenServers или отдельные процессы. Я мог управлять простым состоянием, просто передав его через игровой цикл в качестве аргумента. Это буквально сводилось к чему-то очень простому:

defp game_loop(command, state) do
  # handle the command, maybe modify the state, then...
  command_prompt() 
  |> game_loop(state)
end

Выполните команду (например, возьмите предмет, перейдите в новое место и т. Д.), Предложите игроку ввести следующую команду, повторите. Состояние можно обрабатывать и передавать на следующую итерацию цикла снова и снова. Это было очень похоже на аккумулятор в Enum.reduce/3 функции.

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

Для любого из вас, кто хочет более подробных объяснений того, как работает игра, ниже приводится краткое изложение.

Как работает игра

«Мир» игры определяется .yaml файлами, которые хранятся в каталоге priv/locs. На этом этапе для каждого места нужно только описание и способ определения его выходов. Я придумал что-то вроде следующего:

%YAML 1.2
---
content: >
  You are standing in a grassy clearing on the crest of a gentle
  hill. There is a gravel path leading to a red barn to the south.
exits:
  south: barn

Для непосвященных, если вы отправитесь на юг в этом примере, вы попадете в сарай (т. Е. В другое место). Ожидается, что внутри каталога priv/locs/ будет barn.yaml файл. Если вы не ошибетесь в написании местоположений и случайно не потеряете местоположение, не подключив его, такая настройка должна упростить добавление любого количества местоположений и простую связь между ними.

Эти файлы достаточно легко загрузить и декодировать с помощью вспомогательного модуля, похожего на модель:

defmodule Xventure.Location do
  def load!(name) do
    YamlElixir.read_from_file!("priv/locs/#{name}.yaml")
  end
end

Следует иметь в виду одно предостережение: когда вы декодируете YAML в карты, ключи по умолчанию будут строками, поэтому вам придется использовать соответствующий синтаксис для получения данных.

Помимо этого, возможно, единственное, что стоит упомянуть, это то, что когда вы читаете вводимый пользователем текст с помощью Cowrie (или какого-либо другого инструмента, который получает свои данные из Mix.Shell.IO.prompt/1), он всегда включает новую строку символ (\n), поэтому мы захотим его обрезать, и нам будет проще, если мы нормализуем все команды, сделав их строчными буквами через String.downcase/2.

Соберите все это вместе, и вы сможете перемещаться по вновь созданному миру, полагаясь на предложение функции, подобное следующему:

defp game_loop("go " <> dest, %State{loc: loc} = state) do
  case loc_data(loc) do
    %{"exits" => %{^dest => new_loc}} ->
      game_loop("look", Map.put(state, :loc, new_loc))
    _ ->
      Cowrie.error("You can't go #{dest} from here.")
      game_loop("look", state)
  end
end

Здесь мы видим, что можем обновить state, используя скромный Map.put/3; переход к новому месту назначения приводит к обновлению местоположения пользователя и выполнению команды look.

Думая о будущем

При такой структуре игрового процесса один экземпляр приложения может поддерживать несколько игр. Каждая игра представляет собой однопользовательскую игру без обмена данными между ними, поэтому запуск игры практически ничем не отличается от вызова функции «Hello World».

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

Мысленный эксперимент, который приведет к серьезному рефакторингу, вращается вокруг того, как поддерживать нескольких игроков в одной игре. Быть в курсе…

Код для этой статьи доступен здесь.