Это первая часть из двух частей, посвященных созданию новостных игр с использованием Ink, React и Redux. Мы начинаем с написания нашей истории, а затем во второй части визуализируем ее с помощью React и Redux.

Новостные игры могут быть невероятно сложными, например American Mall Game от Bloomberg, или намного проще, напоминая старые классические приключенческие игры с линейным сюжетом, например, The Uber Game от Financial Times.

В этом руководстве (изначально написанном для встречи Journocoders London March 2018) мы создадим простую игру, используя Ink, предметно-ориентированный язык (DSL) для написания зацикленных сюжетных линий. Затем, во второй части, мы свяжем его вместе в JavaScript, используя InkJS, используя React для рендеринга компонентов и Redux для управления состоянием. Если вы хотите перейти к готовой базе кода, чтобы увидеть, как она выглядит, создайте вилку this Glitch project или клонируйте это репозиторий GitHub. Мы использовали Ink и InkJS в Financial Times, чтобы создать The Uber Game - быстрое воспроизведение даст вам представление о возможностях языка Ink.

Плетение тайны с Inky

Язык Ink с открытым исходным кодом был создан компанией под названием Inkle Studios, которая также выпустила полноценную среду редактирования, которую мы будем использовать при написании наших рассказов. Перейдите на GitHub и загрузите последнюю версию Inky для своей платформы, затем установите и запустите. У вас должен получиться такой экран:

Мы собираемся написать инкрементальную игру, в которой игрок размышляет о жизни киберианского бота-фермера (что, конечно, является отсылкой к приключенческой игре для ПК 1994 года Cyberia , и совсем не насмешливое упоминание о конкретном национальном государстве, которое может быть известно или не быть известно такими бот-фермами, со всеми без исключения сходствами чисто случайным ). Это будет использовать серию игровых циклов и переменных. Наш финал будет, когда бот-фермер будет управлять 760 миллионами ботов, что составляет десятую часть населения планеты Земля. Есть много возможностей для расширения игры - возможно, достижение финала запускает совершенно другой стиль игры, как что-то вроде Универсальных скрепок. Я оставлю это в качестве упражнения для читателя.

Чернильный язык

Ink предоставляет предметно-ориентированный язык (DSL), который представляет собой язык программирования, созданный с учетом конкретного случая использования. Ink DSL очень прост и ориентирован на легкое редактирование текста и написание рассказов. Во многом он похож на Markdown в том, что способствует удобочитаемости, что является полезной чертой при совместном использовании текста проекта с сотрудниками, не являющимися разработчиками, такими как редакторы и субредакторы.

У меня нет возможности добраться до всех нюансов языка Ink, который довольно подробно описан и имеет множество сложных конструкций, полезных для написания сложных диалогов и сюжетных линий. Я настоятельно рекомендую прочитать фантастическую документацию Ink, прежде чем начинать свои собственные проекты.

Мы собираемся в основном придерживаться конструкций языка Ink, которые покажутся вам знакомыми, если вы хоть немного кодировали: переменные, константы, функции и условные выражения. Кроме того, мы будем использовать некоторые уникальные особенности Ink DSL, а именно узлы, теги, варианты выбора и отклонения, которые используются для свяжите кусочки истории вместе и сделайте петли.

Открыв Inky.app, начните новую историю с многострочного комментария, чтобы люди знали, кто написал эту потрясающую игру. Комментарии работают так же, как в JavaScript и других POSIX-совместимых языках:

/**
 * Cyberian Bot Farmer!!!!
 * 2018 Ændrew Rininsland (@aendrew)
 */

Вы также можете использовать однострочные комментарии:

// This is an inline, one-line comment

Технически вам не нужно писать комментарии, чтобы Ink работал, но если вы делитесь своей работой с редакторами и другими журналистами, очень важно, чтобы вы объяснили, что что-то делает, если это не так очевидно. К тому же, вроде, они веселые!

Мы собираемся начать все с использования нашей первой специальной конструкции Ink - divert:

-> Beginning

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

Но сначала давайте настроим несколько глобальных переменных для отслеживания состояния игрока в игре:

// All of our filthy, disgusting global variables lololol
VAR canFarmFocebork = false
VAR canFarmTwurtur = true
VAR twManagers = 0
VAR fbManagers = 0
VAR totalFBBots = 0
VAR totalTWBots = 0
VAR totalRubels = 0
VAR totalFBDudes = 0
VAR totalTWDudes = 0
VAR fbManagerRate = 3
VAR twManagerRate = 6
VAR fbDudeRate = 3
VAR twDudeRate = 5
VAR twBotRate = 3
VAR fbBotRate = 8
CONST MAX_BOTS = 760000000 // INT_MAX in C++/Ink is 2147483647 lol.

Как видите, у нас есть два логических параметра, устанавливающих флаги для типов ботов, которые можно фармить, что дает игроку доступ к рецепту бота «Twurtur» с самого начала. У нас есть два типа менеджеров и ботов-обработчиков (далее «чуваки»), которые совпадают с двумя типами ботов. Чуваки, менеджеры и боты имеют привязанные к ним ставки, которые вы можете настроить во время игрового тестирования, чтобы получить различные типы игровых стилей. Наконец, для нашего критерия конечной игры нам нужна константа, содержащая 750 миллионов².

Давайте сейчас создадим этот узел «Начало». Узел похож на сцену в вашей истории - это место, где принимаются решения и пишется большая часть текста вашей истории. Вы определяете их двумя знаками равенства:

== Beginning ==

Вы можете добавлять подсекции к своим узлам (называемые стежками), используя один знак равенства, но давайте будем простыми и просто полностью этого не делать.

Одна полезная вещь, которую можно поместить под объявлениями узлов, - это теги. Теги содержат метаданные о конструкциях в игре. В нашем коде JavaScript мы можем читать теги и использовать их для таких вещей, как установка фоновых изображений. Добавьте следующую строку:

# background: beginning

Хорошо, время для нашего первого описательного текста!

You have inherited a farm from your uncle {~Jim|John|James|Vladimir}.

Если вы хотите сделать что-то необычное с кодом в тексте истории, вы используете фигурные скобки. В приведенном выше примере мы создаем список параметров, а затем (с помощью оператора ~) выбираем случайный вариант. Время для более описательного текста!

Gazing across the frozen wasteland, you doubt anything will grow here. 
Luckily, you have decent Internet connectivity and have been told there's a growing industry for social media consultants in the area...

Ох, атмосферно…

* [Go to your farm]
    You head towards your farmhouse to plan what to do.
    -> WhatToDo

И, таким образом, мы приходим к нашему первому варианту, который предоставляется игроку в виде списка. Параметры выбора обозначаются либо символом * (который позволяет использовать решение только один раз), либо символом + (для повторяемых решений) в формате неупорядоченного списка. Через секунду вы поймете, что я имею в виду. Текст выбора идет сразу после вышеупомянутого оператора и по умолчанию выводится после принятия решения или подавляется путем заключения текста в квадратные скобки (как я это делаю выше и везде в этой истории, если нет диалога с игроком). После этого вы можете поместить текст для отображения на экране, если этот выбор будет принят, переходя к новой части истории с помощью перенаправления (как мы это делаем выше).

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

Нашим следующим узлом будет узел «Конец», который отвлекает игрока в конце игры. Мы странным образом поместим это в начало, потому что в противном случае Inky будет выдавать ошибки все время, пока мы над этим работаем.

== End ==
# background: ending
IT'S OVERRRRR CONGRATS GO HOME
-> END

Стоит отметить, что это узел под названием End, который затем переходит в состояние под названием END. Последнее имеет особое значение в Ink, так как подразумевает, что цикл истории завершен, а игра окончена. На практике мы используем это, чтобы генерировать исключение в нашем коде JavaScript, которое мы перехватываем, а затем используем для запуска гораздо более сложной конечной сцены. Что вам совершенно необходимо, потому что скучные финальные последовательности СОВЕРШЕННО. THE. НАИХУДШИЙ.

Далее у нас есть WhatToDo узел, куда игрок будет возвращаться много раз и позволяет перемещаться между Городом и фермой игрока.

== WhatToDo ==
# background: farm
{ tick() == true: -> End }
You arrive back at your { farmhouseType() } farmhouse and ponder your next move.
You have a few options here:
+ [Farm a bot]
  -> FarmABot
+ [Go to The City]
  -> GoToCity

Варианты внизу должны показаться вам довольно знакомыми, но на этот раз мы используем несколько функций, чтобы действительно начать игру, которые мы сейчас определим. Функция farmhouseType() является более простой, поэтому мы сделаем это в первую очередь:

=== function farmhouseType ===
    {   
        - totalRubels < 1000:
            ~ return "shanty"
        - totalRubels < 10000:
            ~ return "decrepit"
        - totalRubels < 100000:
            ~ return "modest"
        - totalRubels < 1000000:
            ~ return "swank"
        - else:
            ~ return "incredible"
    }

Вы можете разместить это где угодно. Один из вариантов - создать где-нибудь отдельный functions.ink файл, а затем INCLUDE это, но эта игра достаточно проста, и нам действительно не нужно беспокоиться - я лично просто засунул их все прямо под свои глобальные переменные.

Как видите, функции определяются тремя знаками равенства и словом function. Они также отличаются от узлов тем, что вы можете вернуть значение, а не просто распечатать его (хотя вы определенно можете это сделать). В этом примере у нас есть простое условие if-then-else, которое возвращает дескриптор фермерского дома игрока в зависимости от того, сколько денег он заработал. Логика заключена в фигурные скобки, а условные ветви разделяются тире и заканчиваются двоеточием.

Затем нужно добавить функцию tick(), которая предназначена для обновления статуса игрока на основе каждого хода:

{ tick() == true: -> End }

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

Вот все наши оставшиеся функции. Они действительно говорят сами за себя, я мог бы втиснуть их все в tick(), но разделить их будет легче:

=== function totalBots ===
    ~ return totalFBBots + totalTWBots
=== function totalDudes ===
    ~ return totalFBDudes + totalTWDudes
    
=== function totalManagers ===
    ~ return twManagers + fbManagers
    
=== function fbIncome ===
    ~ return totalFBBots * fbBotRate
    
=== function twIncome ===
    ~ return totalTWBots * twBotRate
=== function tick ===
    ~ totalFBDudes += fbManagers * fbManagerRate
    ~ totalTWDudes += twManagers * twManagerRate
    ~ totalFBBots += fbDudeRate * totalFBDudes
    ~ totalTWBots += twDudeRate * totalTWDudes
    ~ totalRubels += twIncome() + fbIncome()
    Currently you have ₽{totalRubels}, {totalBots()} total bots, {totalDudes()} dudes and {totalManagers()} dude managers working for you.
    ~ return totalBots() > MAX_BOTS

Возможно, вы заметили, что мы даже не стали беспокоиться об аргументах ни в одной из этих функций, потому что снова мы все равно используем целую лодку мерзких глобальных переменных и смеемся, что-то #YOLO? Однако если бы мы использовали аргументы, они бы выглядели так:

=== function herpa(derpa) ===

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

== FarmABot ==
# background: barn
{ tick() == true: -> End }
+ { canFarmFocebork } [Farm a Focebork bot]
    You farm a Focebork bot
    ~ totalFBBots += 1
    -> WhatToDo
+ { canFarmTwurtur } [Farm a Twurtur bot]
    You farm a Twurtur bot
    ~ totalTWBots += 1
    -> WhatToDo
+ Go back home
    ->WhatToDo

На этот экран можно попасть из узла WhatToDo. Снова у нас есть наша tick() функция и куча повторяемых вариантов. Игрок может фармить ботов «Focebork» и «Twurtur» («все сходства чисто случайные и т. Д., Т. Д. И т. Д.»), если у них есть соответствующий рецепт (вы можете вспомнить из ранее мы сразу же даем игроку рецепт «Twurtur»; вы можете легко установить его на false и потребовать, чтобы игрок сначала встретился с определенным персонажем, например, но мы не здесь для краткости).

== GoToCity ==
# background: city
{ tick() == true: -> End }
You make it to The City.
You have ₽{totalRubels} to spend.
What do you want to do?
+ [Hire somebody to make some bots]
    You go to the local tavern
    -> LocalTavern
+ [Buy a recipe]
    You go to Bot Recipes R Us
    -> BotRecipesRUs
+ [Go home to your farm]
    You go home
    -> WhatToDo

Это главное дерево решений, когда игрок посещает Город с узла WhatToDo. Просто tick() и куча повторяемых вариантов. Классно, просто.

== BotRecipesRUs ==
# background: store
{ tick() == true: -> End }
The proprietor of Bot Recipes R Us welcomes you into his establishment.
"Welcome! Welcome!!!"
"I have the finest bot recipes!"
You have ₽{totalRubels} to spend.
* { totalRubels >= 90000 and not canFarmFocebork } [Buy a Focebork recipe (₽90000)]
    ~ totalRubels -= 90000 
    ~ canFarmFocebork = true
    You buy a Focebork recipe.
    -> GoToCity
+ [Go back to the city.]
    { totalRubels < 90000: Your broke ass can't afford anything anyway. }
    -> GoToCity

Здесь у нас есть tick(), некоторый текст диалога и возможность купить рецепт «Focebork», если игрок имеет более 90 000 кредитов и еще не владеет рецептом. У нас также появляется язвительный диалог, когда игроку нужно вернуться в город и он не может позволить себе рецепт.

Это последний большой узел, и это непросто - именно здесь вы нанимаете своих менеджеров и чуваков:

== LocalTavern ==
# background: bar
{ tick() == true: -> End }
A motley crew of surly men sit drinking vodka.
You can:
+ { totalRubels >= 10000 } [Hire a Dude Manager]
    You can hire the following dude managers:
    ++ { canFarmFocebork && totalRubels >= 100000 } [Hire a Focebork dude manager (₽100000)]
        You hire {~Dmitri|Boris|Ralph}
        ~ fbManagers += 1
        ~ totalRubels -= 100000
    ++ { canFarmTwurtur && totalRubels >= 10000 } [Hire a Twurtur dude manager (₽10000)]
        You hire {~Dmitri|Boris|Ralph}
        ~ twManagers += 1
        ~ totalRubels -= 10000
+ { totalRubels >= 100} Hire a Dude
    You can hire the following Dudes:
    ++ { canFarmFocebork && totalRubels >= 1000 } [Hire a Focebork Dude (₽1000)]
        You hire {~Dmitri|Boris|Ralph}
        ~ totalFBDudes += 1
        ~ totalRubels -= 1000
    ++ { canFarmTwurtur && totalRubels >= 100 } [Hire a Twurtur Dude (₽100)]
        You hire {~Dmitri|Boris|Ralph}
        ~ totalTWDudes += 1
        ~ totalRubels -= 100
+ [Go back to the city.]
- You leave the tavern.
    ->GoToCity

Здесь мы представляем два новых элемента: вложенные варианты выбора и сборку. Как вы понимаете, выбор одного из вариантов, обозначенных одним символом +, позволяет вам выбрать один из вариантов под ним, обозначенных ++. Здесь следует отметить, что в языке Ink отступы не делают ничего, кроме как заставить ваших коллег не ненавидеть вас.

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

О, черт возьми, мы уже на стадии JavaScript ?!

да. извиняюсь.

Щелкните здесь, чтобы перейти к Части II.

Эндрю Рининсленд находится в Твиттере по адресу @aendrew и недавно переделал свой блог, используя React, Gatsby и p5.js, чтобы он выглядел совершенно отвратительно.

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

Он также недавно написал книгу о D3, которую по-разному описывали как не для новичков и непонятную.