- PVSM.RU - https://www.pvsm.ru -

Программист, подписывающийся псевдонимом Fleabit, уже полгода разрабатывает свой язык программирования [1]. Сразу же возникает вопрос: ещё один язык? Зачем?
Вот его аргументы:
От Lisp в GameLisp взяты прежде всего простота синтаксиса и простота интерпретатора: реализация GameLisp вместе со «стандартной библиотекой» сейчас занимает 36 KLOC, по сравнению, например, с 455 KLOC в СPython. С другой стороны, по сравнению с обычным Lisp, в GameLisp нет списков и намного меньше ориентации на функциональное программирование и immutable-данные; вместо этого, как и большинство скриптовых языков, GameLisp ориентирован на императивное, объектно-ориентированное программирование.
Синтаксис на основе Lisp с непривычки может вызвать оторопь, но быстро привыкаешь вместо console.print(2 + 2) писать (.print console (+ 2 2)) и т.д. Этот синтаксис намного проще и гибче, чем в привычных скриптовых языках: запятая считается пробельным символом, и может использоваться для улучшения читаемости в любых местах кода; вместо двух видов скобок {}() используются только круглые; большинство знаков ASCII можно использовать в составе символов, так что I~<3~Lisp!~^_^ – допустимое имя для функции или переменной; не нужны; для разделения операций, и т.д. Могу сказать, что безо всякого прошлого опыта с Lisp я всего за пару вечеров сумел переписать классический NIBBLES.BAS [6] на GameLisp: http://atari.ruvds.com/nibbles.html [7]
Всё, что есть в «стандартной библиотеке» GameLisp из средств ввода-вывода – это функция prn для печати на stdout; нет работы ни с клавиатурой/мышью, ни с файлами, ни с графикой, ни со звуком. Предполагается, что пользователь GameLisp сам реализует на Rust все те интерфейсные средства, которые актуальны конкретно в его проекте. В качестве примера такой обвязки на https://gamelisp.rs/playground/ [8] выложен минималистичный движок для браузерных игр, при помощи wasm-bindgen [9] предоставляющий коду на GameLisp функции play:down?, play:pressed?, play:released?, play:mouse-x, play:mouse-y, play:fill и play:draw. В моём порте Nibbles используется тот же самый движок – я лишь добавил к нему функцию для воспроизведения звука. Интересно сравнить размеры: оригинальный NIBBLES.BAS занимал 24 КБ; мой порт на GameLisp занимает 9 КБ; файл на WebAssembly со скомпилированными воедино рантаймом Rust, интерпретатором GameLisp, и кодом игры – занимает 2.5 МБ, и к нему ещё прилагается обвязка на JavaScript в 11 КБ, сгенерированная wasm-bindgen.
Вместе с минималистичным движком на https://gamelisp.rs/playground/ [8] выложены реализации на GameLisp трёх классических игр: pong, тетрис и сапёр. Тетрис и сапёр больше и сложнее, чем мой порт Nibbles, и в их коде есть чему поучиться.
Для демонстрации возможностей GameLisp я выбрал два примера; первый касается макросов. В NIBBLES.BAS уровни заданы стастрочным блоком SELECT CASE со вложенными циклами:
SELECT CASE curLevel
CASE 1
sammy(1).row = 25: sammy(2).row = 25
sammy(1).col = 50: sammy(2).col = 30
sammy(1).direction = 4: sammy(2).direction = 3
CASE 2
FOR i = 20 TO 60
Set 25, i, colorTable(3)
NEXT i
sammy(1).row = 7: sammy(2).row = 43
sammy(1).col = 60: sammy(2).col = 20
sammy(1).direction = 3: sammy(2).direction = 4
CASE 3
FOR i = 10 TO 40
Set i, 20, colorTable(3)
Set i, 60, colorTable(3)
NEXT i
sammy(1).row = 25: sammy(2).row = 25
sammy(1).col = 50: sammy(2).col = 30
sammy(1).direction = 1: sammy(2).direction = 2
...
Все эти циклы имеют похожую структуру, которую можно вынести в макрос:
(let-macro set-walls (range ..walls)
`(do ~..(map (fn1
`(forni (i ~..range) (set-wall ~.._))) walls)))
С этим макросом описание всех уровней сокращается вчетверо, и становится максимально близким к декларативному JSON-подобному описанию:
(match @level
(1 (set-locations '(25 50 right) '(25 30 left)))
(2 (set-walls (20 60) (25 i))
(set-locations '(7 60 left) '(43 20 right)))
(3 (set-walls (10 40) (i 20) (i 60))
(set-locations '(25 50 up) '(25 30 down)))
...
В языке без макросов – например, в JavaScript – аналогичная реализация затуманила бы всё описание уровней лямбдами:
switch (level) {
case 1: setLocations([25, 50, "right"], [25, 30, "left"]); break;
case 2: setWalls([20, 60], i => [25, i]);
setLocations([7, 60, "left"], [43, 20, "right"]); break;
case 3: setWalls([10, 40], i => [i, 20], i => [i, 60]);
setLocations([25, 50, "up"], [25, 30, "down"]); break;
...
На этом примере хорошо видно, насколько код на JavaScript перегружен разнообразной пунктуацией и служебными словами, без которых можно обойтись.
Второй мой пример касается конечных автоматов. Реализация игры у меня имеет следующую структуру:
(defclass Game
...
(fsm
(state Playing
(field blink-rate (Rate 0.2))
(field blink-on)
(field move-rate (Rate 0.3))
(field target)
(field prize 1)
(state Paused
(init-state ()
(@center "*** PAUSED ***" 0))
(wrap Playing:update (dt)
(when (play:released? 'p)
(@center " LEVEL {@level} " 0)
(@disab! 'Paused))))
(met update (dt)
...
(when (play:released? 'p)
(@enab! 'Paused) (return))
...
; Move the snakes
(.at @move-rate dt (fn0
(for snake in @snakes (when (> [snake 'lives] 0)
(let position (clone [[snake 'body] 0]))
...
; If player runs into any point, he dies
(when (@occupied? position)
(play:sound 'die)
(dec! [snake 'lives])
(dec! [snake 'score] 10)
(if (all? (fn1 (== 0 [_ 'lives])) @snakes)
(@enab! 'Game-Over)
(@enab! 'Erase-Snake snake))
(return))
...
(state Game-Over
(init-state ()
(play:fill ..(@screen-coords 10 (-> @grid-width (/ 2) (- 16))) ..(@screen-coords 7 32) 255 255 255)
(play:fill ..(@screen-coords 11 (-> @grid-width (/ 2) (- 15))) ..(@screen-coords 5 30) ..@background)
(@center "G A M E O V E R" 13))
(met update (dt)))))
На каждом кадре (по вызову из window.requestAnimationFrame) игровой движок вызывает метод Game.update. Внутри класса Game определён автомат из состояний Init-Level, Playing, Erase-Snake, Game-Over, каждое из которых определяет метод update по-своему. В состоянии Playing определены пять приватных полей, к которым невозможно обратиться из других состояний. Кроме того, в состоянии Playing есть вложенное состояние Paused, т.е. игра может находиться как в состоянии Playing, так и в состоянии Playing:Paused. Конструктор состояния Paused печатает на экране соответствующую строку каждый раз при переходе к этому состоянию; метод update в этом состоянии проверяет, нажата ли клавиша P повторно, и если нажата и отпущена, то выходит из состояния Paused, возвращаясь к «простому» состоянию Playing. Метод update состояния Playing обрабатывает нажатия клавиш, рассчитывает новое положение игроков, и если кто-то из них врезался в стену, то переходит либо в состояние Game-Over, либо в состояние Erase-Snake. Конструктор состояния Erase-Snake интересен тем, что он принимает параметром ссылку на змейку, которую нужно красиво стереть перед перезапуском уровня. Наконец, у состояния Game-Over конструктор выводит на экран соответствующее сообщение, а метод update пустой – это значит, что независимо от нажимаемых клавиш, ничего нового на экране рисоваться не будет, и выйти из этого состояния невозможно.
Аналогично можно было бы реализовать игру и на классическом скриптовом языке: у класса Game были бы вложенные классы InitLevel, Playing, EraseSnake, GameOver, было бы поле currentState, и метод Game.update делегировал бы вызов currentState.update. Внутри класса Playing был бы вложенный класс Paused, и метод Playing.update в свою очередь делегировал бы вызов подобъекту. Макросы стандартной библиотеки позволяют спрятать автоматическую генерацию полей currentState и делегирующих методов, чтобы разработчик игры видел содержательную реализацию состояний, а не их шаблонное обрамление.
Вместо конечного автомата можно было бы реализовать Nibbles и в виде цикла:
while (lives>0) {
InitLevel;
while (prize<10) {
Playing;
if (dies) {
EraseSnake;
break;
}
}
}
GameOver;
Так и была реализована оригинальная игра на QBasic. Для браузерного движка такой цикл был бы заключён в генератор с yield после отрисовки каждого кадра, а Game.update состоял бы из вызова iter-next!.. Я предпочёл реализацию в виде автомата по двум причинам: во-первых, именно так устроена реализация тетриса, которую автор GameLisp приводит как пример; и во-вторых, в генераторах в GameLisp нет ничего необычного по сравнению с другими скриптовыми языками. Основное предназначение для автоматов – реализация состояний игровых персонажей (ждёт, атакует, убегает и т.д.), невозможная посредством цикла внутри генератора. Дополнительный аргумент в пользу автоматов – изоляция данных, относящихся к каждому из состояний, друг от друга.
Автор: ruvds
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/360278
Ссылки в тексте:
[1] язык программирования: https://gamelisp.rs/
[2] ещё не реализованы: https://github.com/rust-lang/rust/issues/43122
[3] гомоиконичность: https://ru.wikipedia.org/wiki/%D0%93%D0%BE%D0%BC%D0%BE%D0%B8%D0%BA%D0%BE%D0%BD%D0%B8%D1%87%D0%BD%D0%BE%D1%81%D1%82%D1%8C
[4] конечных автоматов: https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D0%B5%D1%87%D0%BD%D1%8B%D0%B9_%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82
[5] недетерминированные: https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%B4%D0%B5%D1%82%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D0%BD%D0%B5%D1%87%D0%BD%D1%8B%D0%B9_%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82
[6] NIBBLES.BAS: https://en.wikipedia.org/wiki/Nibbles_(video_game)
[7] http://atari.ruvds.com/nibbles.html: http://atari.ruvds.com/nibbles.html
[8] https://gamelisp.rs/playground/: https://gamelisp.rs/playground/
[9] wasm-bindgen: https://habr.com/ru/post/353230/
[10] Image: http://ruvds.com/ru-rub?utm_source=habr&utm_medium=article&utm_campaign=Bright_Translate&utm_content=top_10_instrumentov_iot_razrabotki_v_2020#order
[11] Источник: https://habr.com/ru/post/535904/?utm_source=habrahabr&utm_medium=rss&utm_campaign=535904
Нажмите здесь для печати.