Подходы к созданию скриптового языка описания настольных игр

Так уж случилось, что игры я писал лишь для себя, и профессионально этим никогда не занимался.А вот опыт писать DSL (Domain Specific language) для уменьшения рутины написания совершенно разного кода хоть какой-то есть.Именно этим и хочется поделится: как упорядочить необъятное.0df33086707e4d49a5aaf3d512385bec.jpgНаш хороший хабр-юзер GlukKazan пишет много статей о том какие есть замечательные продукты для создания различных досочных игр. Такие как Zillions of Games и Axiom Development Kit.Однако эти программы не универсальны. И всегда хочется улучшить их. Но данные продукты не свободны, поэтому приходится писать программный продукт заново.GlukKazan работает над открытым проектом Dagaz, о чём делится отличными статьями (например тут: Dagaz: Новое начало).Итак, предположим, мы хотим создать универсальный игровой движок для настольных игр, и его основой мы хотим видеть скриптовый язык, который помогает растолковать движку правила игры.Каким мы хотим его видеть?1) Язык должен быть универсальным на сколько можно, дабы описать почти любые правила игры.2) Тем не менее, язык должен быть как можно проще, минимум конструкций.3) Описание правил должны быть легки для чтения игроделу и для написания своих игр4) Для большинства случаев игры можно писать, дополняя/изменяя уже написанные5) Коммуникация (АПИ) со скриптом должна быть насколько простой, насколько это возможно. Так, что бы можно легко писать ботов и ИИ.На первый взгляд кажется, что потраченные усилия вообще никому не нужны будут, поскольку рутину не уменьшить, проще писать игры сразу готовыми.Но это не так.Всё куда проще!

Демиурги и чёрная коробкаЧто ж представим, что мы демиурги, и способны, нет, уже написали этот скриптовый язык. Да, да, уже. Давайте проследим, что этот язык может, и что умеет делать.Не ставьте ограничений там, где их нет Например, проект Dagaz (да и его предшественники Axiom и ZoG) делает акцент на досочных играх. Однако даже человеку объяснить чем досочная игра отличается от недосочной — достаточно сложно. Что уж говорить, описать это в точных определениях на каком-то языке программирования ещё сложнее.Поэтому первое и главное правило — не надо ставить ограничений там, где их нет! Мы будем рассматривать не досочные игры, а настольные.Давайте глянем на следующий список, которые мы хотим описать и играть при помощи нашего движка.Шахматы Сплют Каркассон Точки Пятнашки Домино Перекрашивание кристаллов Дурак Они ну очень разные на первый взгляд. Что же их всех объединяет? Неужели хоть что-то? Да. Объединяет их то, чтоВ один момент времени всегда ходит 1 игрок Почти всегда игрок может думать бесконечно долго (отдельно рассмотрим, когда будут ограничения по времени на ход) Каждый раз существует ограниченное количество возможных действий. Игра Точки является исключением (ибо точку можно ставить где угодно на бесконечной доске). Для удобства мы несколько урежем Точки, а не изменим движок Всё, только эти ограничения на движок! Итого, по сути мы хотим иметь движок, который должен уметь спрашивать 2 основных вопроса: кто сейчас ходит, какие возможные действия он может сделать. IN: who OUT: Player1

IN: actions OUT: [SomeAction1, SomeAction2 Param21, …] И движок должен уметь совершать одно действие — совершить выбранное действие. action SomeAction3 Ну, может ещё одно действие — выбрать предварительное действие (например, если закончится отведённое для хода время, а ход ещё не сделан, дабы не заканчивать игру или выбирать случайное действие, можно выбрать предварительное действие в таком случае). preaction SomeAction5 Не надо ставить больше никаких ограничений, ведь их всё равно кому-то захочется обойти.Думайте шире А как же быть, если мы пишем Покер? Да и в Дурака можно подкинуть карту, а можно не подкинуть. Сказать Пас, или выбрать свою масть — это тоже действие. Выставить поле в Каркассоне — это тоже действие.А тетрис куда? Вот тетрис, наверное, будет делать очень сложно, ибо это реал-таймовая игра, и вряд ли надо. Доработать движок можно легко, но не актуально.Не кастрируйте скриптовый язык Не рекомендую следовать принципу — я получу все данные из скрипта и буду моделировать в движке. Ибо таким образом, проще использовать .ini файл как конфигурацию, а не городить скриптовый язык, так как толку будет столько же.А как взять карту для Дурака, когда отбился? Просто следующим будем снова ходить мы и будет лишь 1 вариант хода — взять карту. Если что можно добавить значения действия авто — то бишь не спрашивать у игрока какой вариант выбирать. IN: who OUT: Player3 # атакует 8й бубна (хотя лучше было бы двумя 10ками — бубовой и чирвой) IN: actions OUT: [Attack 8Diamonds, Attack 10Diamonds 10Hearts, …] IN: action Attack 8Diamonds OUT: action Attack 8Diamonds IN: who OUT: Player4 # забрать со стола, перевести или отбиться? Перевёл IN: actions OUT: [TakeTable, PassShowOnly QueenDiamonds, Defend QueenDiamonds, Pass 6Diamonds, PassShowOnly 6Diamonds,…] IN: action Pass 6Diamonds OUT: action Pass 6Diamonds IN: who OUT: Player1 # предлагает отбить королём или тузом, забрать стол или перевести IN: actions OUT: [TakeTable, Defend KingDiamonds 6Diamonds, Defend KingDiamonds 8Diamonds, Defend AceDiamonds 6Diamonds, Defend AceDiamonds 8Diamonds, PassShowOnly KingDiamonds, Pass KingDiamonds, PassShowOnly AceDiamonds, Pass AceDiamonds] IN: action Defend AceDiamonds 6Diamonds OUT: action Defend AceDiamonds 6Diamonds IN: who OUT: Player3 # предлагает подкинуть 3 му игроку IN: actions OUT: [Pass, Attack AceHearts] IN: action Pass OUT: action Pass IN: who OUT: Player2 # предлагает спасовать, даже автоматически, нечего подкидывать IN: actions OUT: [auto Pass] IN: action Pass OUT: action Pass IN: who OUT: Player4 # пасуем IN: actions OUT: [auto Pass] IN: action Pass OUT: action Pass IN: who OUT: Player1 # предлагает отбить королём или забрать всё IN: actions OUT: [Defend KingDiamonds, TakeTable] IN: action Defend KingDiamonds OUT: action Defend KingDiamonds IN: who OUT: Player3 #берёт из колоды 1 карту, кто ходил и не хватает IN: actions OUT: [auto Take AceHearts] IN: action Take AceHearts OUT: action Take AceHearts IN: who OUT: Player1 #берёт из колоды 2 карты кто отбивался IN: actions OUT: [auto Take 6Hearts 10Clubs] IN: action Take 6Hearts 10Clubs OUT: action Take 6Hearts 10Clubs Пусть движок ничего не знает об игре. Он всего лишь конвейер для запросов и красивого отображения. Не более, но и не менее.Куда же без настроек Насколько бы простым не была коммуникация со скриптом, без настроек обойтись никак не удастся.В Дурака можно играть вдвоём, втроём, вчетвером, впятером и даже вшестером. Можно играть каждый сам за себя, 2×2, 3×3.Нам необходимо посылать настройки. Добавляем ещё одну команду. Ок, две, надо же и проверить состояние set players = 4

set groups = 2×2

set startFrom = Player3

IN: get name OUT: name = 15-puzzle Игра прежде всего Необходимо помнить, что мы создаём игры. А любые игры объединяет то, чтоИгра может быть не начата Игра может идти Игра может быть завершена и иметь итоговый результат Создадим ещё несколько команд, которые отражают новые способности — загрузить игру, начать игру, узнать результат игры IN: load /path/chess.game OUT: load /path/chess.game

IN: start OUT: start

IN: result OUT: Finished; Win Player1; Details Player1 78, Player2 38 OUT: Loaded /path/chess.game OUT: Started Отделить мух от котлет. Язык визуализации Не стоит гнаться за излишней универсальностью.Будем помнить, что ботам визуализация по барабану, а вот человек очень придирчив, и хочет видеть очень красивую картинку.Главный вывод из этого — языки визуализации и языки обмена сообщениями в общем случае различны. Гнаться за универсальностью скриптового языка не стоит, задачи ведь разные.Язык сообщений визуализации не надо подгонять под язык обмена.Пусть вы создали, что на каждый необходимый для визуализации объект есть формат визуализации (далее ФВ), который описывает какую картинку (или какие картинки) следует отобразить, в какой части экрана, что с чем наложится.Нам вначале необходимо отобразить всё. Значит, необходима команда визуализация ситуации, которая выдаёт список объектов в формате ФВ.Ведь мы должны увидеть шахматную доску с фигурами, или что нам раздали для Дурака. IN: view OUT: [ (Piece1, ФВ), (King, ФВ), … ] Кстати, визуализировать зачастую надо то, что не фигурирует в качестве фигур в самой игре — например для Дурака надо отобразить количество закрытых карт у соперников, а так же колоду.Кроме того, необходимо знать как визуализировать возможные ходы.То есть должна быть команда визуализировать действия, который содержит список всех возможных действий, и к каждому из них в одном из двух вариантов новое описание: полное или частичное (с добавлением необходимых фигур отображения, и убирания фигур из имеющихся).Например, в Дурака необходимо убрать из колоды выбранную карту, зато эта карта должна появиться на столе. IN: view actions OUT: [(Attack 8Diamonds, Diff [remove Card5, add (Card5, ФВ), … ]), … ] А в шахматах, выбранная фигура должна стать «выбранного» цвета, и на месте выбранной клетки появится эта же фигура. В случае атаки к тому же должна исчезнуть битая фигура.Кроме того, необходимо знать, когда надо отображать то или иное возможное действие.Собственно, либо в предыдущую команду засунуть, либо новую команду добавить. IN: view actions OUT: [(Attack 8Diamonds, Diff [remove Card5, add (Card5, ФВ), … ], OnChoose Card5), (Pass, Diff [add PassWord], OnChoose PassLabel), … ] Язык искусственного интеллекта Модули ИИ желательно писать отдельно, но интегрировать в общий скрипт.Разговор ИИ в принципе будет не сильно сложный. IN: analize OUT: [75 Pass, 44 Attack 6Diamonds, 59 Attack 8Diamonds] IN: analize quick OUT: [10 Pass, 5 Attack 6Diamonds, 20 Attack 8Diamonds] Реализовать подобное куда сложнее, чем описать. Вполне возможно понадобятся форки.Обоюдное понимание Необходимо, чтобы движок понимал бы, совершает ли он ошибки в разговоре со скриптом, или нет. Связь должна быть обоюдной, особенно когда взаимодействие представляет клиент-серверное приложение.По сути необходимо одна команда: последняя принятая команда. IN: it OUT: result Этой же командой следует отвечать и тогда, когда ответ не важен. То есть установках, принятых действиях.А то кто-то поставил лимит времени на обдумывание, игрок проспит это время, походит, а движок уже походил случайно за игрока.Не забываем, что и движок может ответить ошибкой. IN: action TakeTable OUT: ERROR action is out list

IN: who OUT: ERROR game result is Finished Не боги горшки обжигают Что же, мы достаточно много проследили за тем, что должен уметь наш скрипт, а на что нам наплевать.Вся, не, ВСЯ логика ложится на скрипт. Так и только так мы добьёмся полной универсальности любых настольных игр.Любой компромисс тут ведёт к ограничениям, из-за которых не раз и не два придётся плясать с бубнами.Но мы же хотели лёгкий, простой язык! А нам предлагают всё-всё положить на игроделов.Если нельзя, но очень хочется, то можно! Сложно должно быть программисту, а не игроделу. И так будет.Будьте игроделами Будьте немного игроделом сами! Одно из самых удивительных свойств Си, который меня до сих пор поражает, это то, что тип string — библиотечная функция.Не надо ждать, пока игроделы придумают что такое доска, поле или колода карт. Напишите на скриптовом языке сами эти понятия.Обычные люди просто напишут, возмём доску, и создадим тут такие поля.Именно поэтому и необходимо, чтобы все написанные скрипты можно было поверх изменять.Захотелось для игры не статическую доску, а динамическую, пожалуйста, изменяем доску так, что её состояние будет проверятся каждый раз на ход. # DynamicBoard import StaticBord

let board { constuctor {let board = prevous board} on_player_change {let board = recalculate} } Кесарю кесарево, а Богу Богово Даже не смотря на кучу написанных библиотек самим программистом, скриптовый код станет облегчённым, но не лёгким.Как убрать ненужные куски кода? Необходимо понять, что нам нужен подъязык, DSL этого скриптового языка, написанном на самом языке.Звучит страшно, однако не всё есть чёрт, что малюют.Для меня одной из самых красивых библиотек является парсер Parsec для Хаскеля. Это куда красивее реализации строк в Си.Строки хоть планировали включать в язык, а вот парсер писать не планировали, когда составляли Хаскель.По сути, там создали лишь средствами языка 2 уровня подъязыка для составления парсера (на самом деле там ещё есть не менее 4 дополнительных уровней).1) Уровень токенов/символов. На этот малопонятный большинству уровень почти никто не лезет писать свои функции, библиотечных функций хватает с головой.Например, конец ли строки — eofвзять 1 токен/символ — anyTokenПопробовать парсер, если упадёт, сделать вид, что он не потреблял токены — try pИли: попробовать парсер, если он упадёт без потребления токенов, применить второй парсер — p1 <|> p2Посмотреть вперёд: попробовать парсер, сделать вид, что он не употребил токенов, если парсер удачный — lookAhead p (в реальности там намного больше функций).Комбинацией этих токенистических функций позволяет порождать очень сложные парсеры.2) Уровень парсеров. Итак интуитивно понятный, дополненный целым зоопарком функций: такими какмного парсеров — many pмного, хотя бы 1 парсер — many1 pпарсер разделённый — p `sepBy` seppстрочка — stringмежду парсерами — between p1 p2…Собственно, когда приходит юзер библиотеки, то видит, что «всё построено до нас», есть уже готовый конструктор, просто бери и собирай то, что тебе нужно и на том уровне, который тебя интересует.То же самое касается и нашего языка. Не должен игродел думать как писать функцию фильтра, она должна быть там уже написана.Просто бери и пользуйся, так должен говорить скриптовый язык своим юзерам! Если хочу шашки, но чтобы в ячейку помещалось 1 или 2 шашки — надо мне пересоздать лишь описание ячейки.Хочу квадратную доску — загружаю себе квадратную, хочу шестиугольную — загружу 6-угольную.Надо мне, чтобы в зависимости от снятия пешек с доски уничтожались поля самой доски — делаем доску динамической и следим за уничтожением пешек.

Суп быстрого приготовления или нет предела совершенству Мы все любим вкусно поесть, но вот вкусно готовить могут не все. Это требует умений и времени.Игроделы такие любители супов.Им надо ещё облегчить долю.Необходим в нашем скиптовом языке ещё… мета-язык. Выдохните, просто шаблоны игр.Шахмато-подобные-игры, Шашки-подобные-игры, дурак-подобные-игры, карточные-игры, …Дайте возможность просто сконфигурировать готовые решения, а не заниматься программированием. import ChessLikeGame;

let params = { previous params; players = 4; board_length = 75 symmetric start positions = true; start_white_positions = [A1, C1, D1, …] } Заключение Что же, если посмотреть на наши хотелки вначале статьи, то можно ужаснуться, мы хотели иметь и супер-универсальный скриптовый язык, и одновременно быть по-сути конфигурированием готовых решений, легко менять логику написанных игр.На самом деле мы поняли чего хотим, чего не хотим, поняли, что основную часть написанного кода надо писать на скриптовом языке, а движку оставить визуализацию и реакцию.Нет никакой разницы между шахматами, Балдой и дураком.Кто ходит? Игрок 1

IN: who OUT: Player1 Нет разницы между точками и пятнашками кристалл-перекрашиванием. Есть ограниченный выбор действий каждый раз. # chess action Castle King Rook2

# checkers action Attack Men7 D2 F4

# splut action Pull Troll E5 D5

# points action Add 10–14

# carcassonn action Put Title1Tree Place8–17

#15-puzzle action Push 8

# dominos action Put Title1–6 Place4–1 Left

# crystall painting action Color Yellow

# durak action New QuenHearts Это просто надо понять, и тогда станет ясно, как это объяснить компьютеру.

© Habrahabr.ru