Превозмогая трудности: Gravity Defied на sed

    image Итак, эта статья посвящается тем, кто любит решать нестандартные задачи на не предназначенных для этого инструментах. Здесь я опишу основные проблемы, с которыми столкнулся во время создания аналога игры Gravity defied с использованием потокового текстового редактора (sed).

    Далее предполагается, что читатель хотя бы немного знаком с синтаксисом sed'ом и и написанием скриптов под bash.

    Мирный вечер декабря перестал быть мирным, когда мне пришло сообщение от преподавателя примерно такого содержания:
    На sed:
    Gravity defied

    Это должно быть круто

    Признаться, первые полчаса я сидел с мыслью о том, как это вообще возможно. Но потом мне удалось взять себя в руки и я начал разбираться.

    Попытки гуглить на тему игр на sed привели к арканоиду и сокобану.

    Прежде, чем мы начнём разбор проблем, хочу поделиться репозиторием с проектом и видео-демонстрацией результата

    Итак, Проблема первая: представление в памяти sed должен как-то хранить текущее состояние игры. В нашем распоряжении два места для магии hold space и pattern space.

    Hold space будет хранить состояние игры между итерациями (итерацией я буду называть обработку одного входящего символа), а в pattern space мы будем изменять состояние игры.
    Алгоритм примерно такой:

    1. Переходим к действию, которое привязано к символу, который мы получили на вход
    2. Записываем в pattern space содержимое hold space
    3. Изменяем содержимое pattern space в соотвествии с логикой действия
    4. Записываем содержимое pattern space в hold space
    5. Производим наложение эффектов на pattern space (на этом шаге мы из нашего «служебного» состояния игры в то, что будет видеть пользователь)
    6. Выводим содержимое pattern space на экран
    7. Повторить с п.1 для каждого введённого символа

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

    Первым делом — инициализация. Создадим метку 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)/F\1B/
    

    Тут вся суть в цифре 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-последовательностей, я ещё дополнительно воспользовался таблицей 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.

    Алгоритм работы:

    1. Если любая часть игрока ближе, чем на N символов к правому краю карты, переходим к следующему пункту
    2. Удаляем второй символ карты (первый у нас — рамочка)
    3. К концу каждой строки, перед цифрой добавляем #
    4. Если у нас набралось ровно M символов #, то выполняем следующий пункт, иначе — пропускаем
    5. Проверяем номер текущей зоны и заменяем все # на соответствующую данной зоне карту, меняем имя зоны на имя следующей зоны
    6. Переходим к метке end
    7. На этапе пост-процессинга обрезаем видимую часть так, чтобы символы # никогда не попадали в видимую область, а так же удаляем вспомогательные данные, например, номер зоны.

    Ура! Теперь у нас есть базовые знания, как создать игру на sed. Зачем? Потому что можем.

    P.S. Задание любезно предоставлено Жмылёвым С.А. Надеюсь, следующие поколения примут часть моего опыта и сделают что-нибудь ещё более замечательное. х)
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 8

      +2
      А вы знаете толк.
      Стоит добавить в хаб ненормальное программирование.
        +1
        Добавил.
        +1
        А можно видео?
        0
        Тоже баловался играми в консоли. У меня получилась такая вот поделка на баше
          0
          Можно поподробнее про преподавателя? Это преподаватель какой то дисциплины в вузе или наставник, репетитор?
            0
            Преподаватель в ВУЗе.
            0
            Оперативно написал. Осталось узнать, зачтут ли. )

            На мой взгляд, править код с невводимыми с клавиатуры символами трудновато.

            Я у себя разбил логику на несколько вызовов sed'а и отправлял результат по пайпу в процедуру раскраски. Вышло не так круто, зато появилась возможность создать несколько цветовых схем, свободно с ними экспериментировать и дать пользователю выбор расцветки. Добавляет гибкости, да и код становится понятней.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое