- PVSM.RU - https://www.pvsm.ru -
Итак, эта статья посвящается тем, кто любит решать нестандартные задачи на не предназначенных для этого инструментах. Здесь я опишу основные проблемы, с которыми столкнулся во время создания аналога игры Gravity defied с использованием потокового текстового редактора (sed).
Далее предполагается, что читатель хотя бы немного знаком с синтаксисом sed'ом и и написанием скриптов под bash.
Мирный вечер декабря перестал быть мирным, когда мне пришло сообщение от преподавателя примерно такого содержания:
На sed:
Gravity defied
…
Это должно быть круто
Признаться, первые полчаса я сидел с мыслью о том, как это вообще возможно. Но потом мне удалось взять себя в руки и я начал разбираться.
Попытки гуглить на тему игр на sed привели к арканоиду и сокобану [1].
Прежде, чем мы начнём разбор проблем, хочу поделиться репозиторием с проектом [2] и видео-демонстрацией [3]результата
Итак,
Проблема первая: представление в памяти
sed должен как-то хранить текущее состояние игры. В нашем распоряжении два места для магии hold space и pattern space.
Hold space будет хранить состояние игры между итерациями (итерацией я буду называть обработку одного входящего символа), а в pattern space мы будем изменять состояние игры.
Алгоритм примерно такой:
Для упрощения разбора введённого текста примем на веру, что из всей входящей строки лишь первый символ нам важен.
Первым делом — инициализация.
Создадим метку print, которая будет создавать поле игры в начальный момент времени. С момента запуска игры лишь один раз возникнет ситуация, когда на вход sed'у передаётся пустая строка: самый старт игры.
Таким образом,
/^$/b print
...
:print
# Начало любого действия, которое иницируется извне
g
s/.*/
+-----------------------+
|BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB1
|BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB2
|BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB3
|BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB4
|BBBBBBBBBBBBBBBBBBBBBUPPABBBBBBBBBBBB5
|BBBBBBBBBBBBBBBBBBBBUBBBABBBBBAPPPPPP6
|DBBBBBBBBBBBBBBBBBUPBBBBABBBBBABBBBBB7
|BDBSBFBBBBBBBBBBBUBBBBBBABBBBBABBBBBB8
|BBPPPPPPPPPPPPPPPBBBBBBBPPPPPPPBBBBBB9
+-----------------------+
b end
На этом этапе всё зависит от вашего воображения. Вы сами решаете, за что отвечает каждый символ. У меня B — это пустое место, F и S — колёса байка, в A, D, P, U — дорога (четыре вида, для красоты, но об этом — позднее).
Нам необходимо вывести всё полученное на экран. Как вы могли заметить, в конце print мы переходим к метке end.
end — это общее завершение любого действия.
:end
# Сохраняем все изменения в hold space
h
# Здесь позднее провернём всю пост-обработку нашего игрового пространства
# Отправляем символ очистки экрана
i
^[[H
# Печатаем содержимое pattern space на экран
p
Примечание: ^[[H не стоит копипастить, это escape-последовательность. Например, в vim она вводится так: Ctrl+V Ctrl+ESC [ H
Запустим наш скрипт с помощью
sed -nf gravity.sed
Поздравляю с статической картинкой!
Когда у нас есть поле, достаточно просто написать команды, которые будут двигать влево-вправо наши импровизированные колёса:
s/FB/BF/
s/SB/BS/
Движение вверх чуть сложнее но мы же не боимся сложностей, правда?
s/B(.{39})F)/F1B/
Тут вся суть в цифре 39. Это количество символов в строке.
Добавляем пару меток и «привязываем» их к нужным клавишам, и вуаля, у нас есть некий абстрактный байк (ладно, два колеса), для которого не существует границ и физики. Но если вы захотите писать лабиринт, то вам как раз это и нужно.
Проверить игру не сложно, но нажимать Enter после каждого введённого символа — удовольствие ниже среднего, так что нужно автоматизировать этот процесс.
Проблема вторая: тактование
Так как «сердце» игры — sed, нужна оболочка, которая за нас будет нажимать enter каждый раз, когда мы нажали кнопку. Бесконечный цикл — самое оно.
Примерный код:
(while true
do
read -s -n 1 key # считываем одно нажатие клавиши без вывода на экран в переменную key
echo $key
done) | sed ...
Игра теперь будет станет чуть более радостной, но в ней всё ещё есть большой недочёт: игрок может влиять на ход времени. Чем быстрее тыкает игрок по клавишам, тем быстрее ход игры. Нас такое не устраивает, поэтому нужно тактование. Теперь у нас два источника данных — тактовый генератор и пользователь. Самое простое решение, которое приходит в голову — воспользоваться ключом -t у read. Если пользователь ничего не введёт за указанное кол-во секунд, то read не станет блокировать скрипт. Это решение меня не устроило: на SunOS read отказывался принимать дробное количество секунд, а динамичная игра с одним кадром в секунду — это как-то странно. Второе решение — использовать именнованый pipe:
# Удаляем (на всякий случай) pipe и создаём новый
rm -f gravity-fifo;
mkfifo gravity-fifo;
# Эта строчка будет держать pipe открытым достаточно долго
sleep 99999999 > gravity-fifo &
# Запустим игру
sed -nf gravity.sed gravity-fifo &
# Тактовый генератор, который раз в $TIME * 10^-6 секунд будет записывать символ t в pipe
while true
do
echo t > gravity-fifo
usleep $TIME
done &
# Пользовательский ввод
(while true
do
read -s -n 1 key
echo $key
[[ $key == "q" ]] && pkill -P $$
done ) | $SED -u -e '/t/d' > gravity-fifo
Немного пояснений:
pkill — хороший способ убить тактовый генератор и sleep.
А если вам непонятно, зачем нужен этот sleep, то можете проверить без него: с первым же echo pipe закроется и sed поймает EOF. Попутно мы запрещаем пользователю писать тактирующий символ — мы тут байк водим, а не временем управляем.
Проблема третья: физика
У нас есть тактирующий символ, который вызывается через константные промежутки времени. Именно в обработчике этого символа можно прописать всю физику игры. Тут не могу дать общих советом, вся физика — это набор регулярок, которые проверяют всё, что проверяется.
Проблема четвёртая: пост-обработка
Сразу после того, как мы перешли к метке end и сохранили изменения в hold space, мы можем приступать к наложению эффектов. Ранее я упоминал, что я использую четыре типа дорог. К этому я пришёл методом проб и ошибок. В первых версиях дороги были одного типа: R, а на этапе пост-обработки я пытался написать регулярки, которые бы делали подъем/спуск в зависимости от взаимного расположения дорог.
Идея была отвергнута: алгоритм постоянно сбоил, проще прописать тип дорог.
Вооружаемся таблицей ANSI Escape-последовательностей [4], я ещё дополнительно воспользовался таблицей Unicode и получилось…
s/A/^[[107;38;5;82m█^[[0m/g
s/D/^[[107;38;5;82m▚^[[0m/g
s/P/^[[107;38;5;82m▀^[[0m/g
s/U/^[[107;38;5;82m▞^[[0m/g
Подводные камни есть и здесь: при использовании юникода pattern поиска не должен содержать точное количество символов. Unicode-символы распознаются как два символа и логика такой регулярки ломается.
Проблема пятая: маленькое пространство
На экран у нас влезает не так уж много символов, а карту хотелось бы сделать больше. Здесь на помощь приходит Scroll Buffer. Это такое место, невидимое для пользователя, которое будет хранить в себе кусочек продолжения карты. Для комфортного скроллинга стоит пронумеровать строчки, а в самом конце добавить строку, которая нумерует зону, например, z1
Алгоритм работы:
Ура! Теперь у нас есть базовые знания, как создать игру на sed.
Зачем? Потому что можем.
Автор: Firemoon
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-programmirovanie/220966
Ссылки в тексте:
[1] арканоиду и сокобану: https://github.com/aureliojargas/sed-scripts/
[2] репозиторием с проектом: https://github.com/Firemoon777/gravity-defied
[3] видео-демонстрацией : https://asciinema.org/a/9clhjp8g01qg4vo5if80xhbkn
[4] таблицей ANSI Escape-последовательностей: http://ascii-table.com/ansi-escape-sequences.php
[5] Источник: https://habrahabr.ru/post/317638/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.