Підходи до створення скриптової мови опису настільних ігор

Так вже сталося, що ігри я писав лише для себе, і професійно цим ніколи не займався.
А ось досвід писати DSL (Domain Specific language) для зменшення рутини написання абсолютно різного коду хоч якийсь є.
Саме цим і хочеться поділиться: як упорядкувати неосяжне.

Наш хороший хабр-юзер GlukKazan пише багато статей про те, які є чудові продукти для створення різних досочных ігор. Такі як Zillions of Games та Axiom Development Kit.
Однак ці програми не універсальні. І завжди хочеться покращити їх. Але ці продукти не вільні, тому доводиться писати програмний продукт заново.
GlukKazan працює над відкритим проектом Dagaz, про що ділиться відмінними статтями (наприклад тут: Dagaz: Новий початок).

Отже, припустимо, ми хочемо створити універсальний ігровий движок для настільних ігор, і його основою ми хочемо бачити скриптова мова, який допомагає розтлумачити движку правила гри.
Яким ми хочемо його бачити?
1) Мова має бути універсальним на скільки можна, щоб описати майже будь-які правила гри.
2) Тим не менш, мова повинна бути як можна простіше, мінімум конструкцій.
3) Опис правил повинні бути легкі для читання игроделу і для написання своїх ігор
4) Для більшості випадків гри можна писати, доповнюючи/змінюючи вже написані
5) Комунікація (АПІ) зі скриптом повинна бути наскільки простий, наскільки це можливо. Так, що б можна легко писати ботів і ШІ.
На перший погляд здається, що витрачені зусилля взагалі нікому не потрібні будуть, оскільки рутину не зменшити, простіше писати гри відразу готовими.
Але це не так.
Все набагато простіше!

Демиурги і чорна коробка

Що ж уявімо, що ми демиурги, і здатні, ні, вже написали цей скриптова мова. Так, так, вже. Давайте простежимо, що ця мова може, і що вміє робити.

Не ставте обмежень там, де їх немає

Наприклад, проект Dagaz (та і його попередники Axiom та ZoG) робить акцент на досочных іграх. Однак навіть людині пояснити чим досочная гра відрізняється від недосочной — досить складно. Що вже говорити, описати це в точних визначеннях на якійсь мові програмування ще складніше.
Тому перше і головне правило — не треба ставити обмежень там, де їх немає!
Ми будемо розглядати не досочные гри, а настільні.
Давайте глянемо на наступний список, які ми хочемо описати і грати при допомоги нашого двигуна.
  • Шахи
  • Сплют
  • Каркассон
  • Точки
  • П'ятнашки
  • Доміно
  • Перефарбування кристалів
  • Дурень
Вони ну дуже різні на перший погляд. Що ж їх об'єднує? Невже хоч щось?
Так. Об'єднує їх те, що
  1. В один момент часу завжди ходить 1 гравець
  2. Майже завжди гравець може думати нескінченно довго (окремо розглянемо, коли будуть обмеження по часу)
  3. Кожен раз існує обмежена кількість можливих дій. Гра Точки є винятком (бо точку можна ставити де завгодно на нескінченній дошці). Для зручності ми кілька уріжемо Точки, а не змінимо движок
Всі, тільки ці обмеження на движок!
Разом, по суті ми хочемо мати движок, який повинен вміти запитувати 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: Take action AceHearts
OUT: Take action AceHearts
IN: who
OUT: Player1
#бере з колоди 2 картки хто відбивався
IN: actions
OUT: [auto Take 6Hearts 10Clubs]
IN: Take action 6Hearts 10Clubs
OUT: Take action 6Hearts 10Clubs

Нехай движок нічого не знає про гру. Він всього лише конвеєр для запитів і красивого відображення. Не більше, але й не менше.

Куди ж без налаштувань

Наскільки б простою не була комунікація зі скриптом, без налаштувань обійтися не вдасться.
В Дурня можна грати вдвох, втрьох, вчотирьох, вп'ятьох і навіть вшістьох. Можна грати кожен сам за себе, 2х2, 3х3.
Нам необхідно посилати налаштування. Додаємо ще одну команду. Ок, дві, треба ж і перевірити стан
set players = 4

set groups = 2x2

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: actions view
OUT: [(Attack 8Diamonds, Diff [remove Card5, add (Card5, ФВ), ... ]), ... ]

А в шахах, обрана фігура повинна стати «вибраного» кольору, і на місці вибраної клітини з'явиться ця ж фігура. У разі атаки до того ж повинна зникнути бита постать.
Крім того, необхідно знати, коли треба відображати те чи інше можливе дію.
Власне, або попередню команду засунути, або нову команду додати.
IN: actions view
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

Це треба просто зрозуміти, і тоді стане ясно, як це пояснити комп'ютера.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.