В комментарии к моей предыдущей статье, «Какие задачи не решаются bat-файлами?», предположили, что на bat-файлах не получится написать Doom. Насчет Дума я пока не уверен, а вот тетрис у меня получился.

Сразу оговоримся, что код, который мы будем разбирать – это proof of concept. Он имеет недоработки, но я намеренно оставил его таким, чтобы не усложнять.

Bat-файл выложен на Яндекс-диск: ссылка

Upd. На Яндекс-диске превышен лимит скачивания. Вот тот же файл на Google Drive

Геймплей
Геймплей

Ввод с клавиатуры

В играх определенных жанров есть необходимость проверять, какие клавиши нажаты в данный момент, и при этом не останавливаться в ожидании нажатия. В С++ это делается всего одним вызовом GetKeyboardState(). В пошаговых играх и в играх типа тетриса может пригодиться посимвольный ввод, т.е. ожидание ввода одного символа без ожидания нажатия на Enter. В C++ это тоже один вызов – _getch(). Поэтому обидно, что в bat-файле ничего подобного сделать нельзя.

Но у нас есть choice.exe. Эта утилита ожидает ввода одного из перечисленных символов и выставляет errorlevel, равный порядковому номеру выбранного варианта. Допускаются только буквы и цифры. Есть опция таймаута. При этом требуется указать вариант по умолчанию, который выбирается по истечении таймаута.

Напрашивается решение: использовать клавиши WASD, как обычно в играх, для движения. Только движения вверх в тетрисе нет, поэтому W у нас будет поворот. При этом S, т.е. вниз, будет вариант по умолчанию, который будет срабатывать каждую секунду.

choice /C "sawd" /D s /T 1 > NUL

Вывод перенаправлен в NUL, потому что подсказку по управлению мы выведем покрасивее и сбоку от игрового поля.

И тут возникает ограничение на скорость игры. Таймаут указывается как целое количество секунд. То есть, минимальный таймаут – одна секунда. Значение 0 допустимо, но в этом случае всегда возвращается вариант по умолчанию, и никакого ввода не получается.

Кроме того, таймаут отсчитывается от запуска choice.exe. Иными словами, от предыдущего ввода, так как у нас эта команда будет в бесконечном цикле. Таким образом, если постоянно нажимать вправо-влево, то фигура вообще перестает двигаться вниз. Это первый недостаток моей реализации игры.

Представление игрового поля

В bat-файлах нет массивов. Однако, можно использовать переменные с именами, содержащими индексы. Таким образом организуются псевдо-массивы. В нашем случае это двумерный массив field_%Y%_%X%. Y меняется от 0 до 15, причем, 0 – это верх. X – от 0 до 7. Элементы равняются либо 0 – блок свободен; либо 1 – блок занят.

Перед началом игры поле инициализируется нулями:

for /L %%a in (0,1,15) do (
  for /L %%b in (0,1,7) do set field_%%a_%%b=0
)

Рендеринг игрового поля

Псевдографика позволит изобразить по две строки поля в одной строке текста. Половина ячейки символа – один блок. Так блоки получаются более-менее квадратными. Символ с кодом 223 представляет собой закрашенную верхнюю половину ячейки, 220 – нижнюю, а 219 – полностью закрашенная ячейка.

В bat-файлах нет способа указать код символа цифрами, зато можно вставить псевдографику как есть. Для интерпретатора это обычные символы, которые могут быть частью переменной или параметра команды.

Напомню, что bat-файл надо сохранять в кодировке 866. Кодировку можно изменить командой chcp, но нам это не понадобится.

Когда обе половины ячейки свободны, нужно вывести пробел. Это значит, в скрипте будет строка, оканчивающаяся пробелом. Его не видно, а он есть, и есть риск его удалить при редактировании. В кавычки его ставить нельзя. Если с командами SET и ECHO использовать кавычки, они попадают, соответственно, в переменную и на экран.

Перемещение курсора

Чтобы обновить весь экран, есть вариант его очистить командой CLS, а потом вывести всё содержимое. Но в нашем случае рендеринг идет дольше смены кадров, и изображение мигает, если очищать экран. Поэтому нужно переместить курсор в точку (1;1) и вывести новое поле поверх старого.

Для перемещения курсора нам понадобится код ANSI-терминала <ESC>[1;1H. <ESC> – это символ с кодом 27 (033 или 0x1B). Его, как и псевдографику, можно вставлять в команды как есть. Главное – найти текстовый редактор, который позволит это делать. Notepad++ подойдет. Спасибо @horror_x за подсказку.

Алгоритмы

Написание своей реализации тетриса – это упражнение не столько на графику, сколько на алгоритмы. Нужно определиться с представлением фигуры в памяти, рассчитывать поворот фигуры и ее коллизии с «ландшафтом».

Выбор фигуры

В таблице ниже представлены все фигуры тетриса

Все фигуры
Все фигуры

Случайным образом (через переменную %RANDOM%) выбираются значения переменных figtype (от 0 до 3) и figflip (0 или 1). Для figtype от 0 до 2 изменение figflip – это зеркальное отражение относительно вертикальной оси. Фигуры с figtype 3 симметричны относительно обеих осей, поэтому для этого случая figflip выбирает саму фигуру. Это позволяет не кодировать явно все 8 фигур. Достаточно пяти. Фигуры с figtype<3 и figflip=1 получаются отражением.

Представление фигур

Функция :getfigure заполняет переменные figdefx и figdefy. В каждой из них 4 цифры. Каждая цифра – координата. figdefx – это X-координаты, а figdefy – Y. Таким образом закодированы положения 4 блоков, из которых состоит фигура. Эти переменные не меняются при повороте фигуры по ходу игры, и figflip их тоже не меняет. Вместо этого координаты преобразуются при помощи матрицы поворота и отражения.

Матричная алгебра

:getfigure инициализирует матрицу либо единичной матрицей, либо матрицей отражения в зависимости от figflip. Матрица хранится в 4 переменных: mat11, mat12, mat21 и mat22.

При нажатии на W происходит умножение матрицы поворота на 90° по часовой стрелке на текущую матрицу. При умножении матриц от перестановки множителей произведение меняется. Первой должна идти матрица, описывающая преобразование, выполняемое последним.

Если поворот приводит к коллизии, выполняется поворот в другую сторону аналогичным образом. В общем случае в играх так не делают, потому что накопление погрешности вычислений. Но у нас частный случай, когда все числа целые, повороты только на 90°, и элементы матрицы могут равняться только 0, 1 и -1.

Способ кодирования фигур не допускает отрицательных координат. Вся фигура занимает один квадрант и поворачивается относительно своего конца, а не центра. Это еще одна вещь, которую я не исправлял.

Применение фигуры

Функция :applyfigure принимает 3 параметра: колбэк и координаты. Функция рассчитывает координаты каждого блока фигуры с учетом указанного смещения и матрицы и вызывает колбэк, передавая ему эти координаты.

Используются следующие колбэки:

  • :setblock заполняет блок, т.е. присваивает значение 1 элементу псевдо-массива field_%2_%1. Таким образом, фигура рисуется на поле;

  • :clearblock очищает блок. Фигура стирается с поля;

  • :testcollision возвращает errorlevel 1 если указанный блок заполнен или находится за границами поля. Коллизия с верхним краем тоже проверяется. Она может произойти при повороте фигуры.

Если колбэк возвращает errorlevel 1, :applyfigure прерывает выполнение и тоже возвращает 1.

Жизненный цикл фигуры

На метке :nextfigure после выбора очередной фигуры эта фигура рисуется на поле в точке (3; 0). Если при этом возникает коллизия, игра заканчивается. Как вы помните, координаты блоков в определении фигуры неотрицательны, а флаг figflip отражает фигуру относительно вертикальной оси. Таким образом, коллизия с верхним краем исключена. Коллизии с правым и левым краем тоже исключены, потому что нет таких широких фигур. Таким образом, коллизия может быть только с ландшафтом.

Если при повороте фигуры, движении вправо или влево происходит коллизия, движение или поворот не выполняется (в случае поворота вычисляется поворот обратно).  Помните, что точка, вокруг которой поворачивается фигура, – это не центр фигуры. Если фигура не поворачивается, отодвиньте ее от края.

Если коллизия происходит при движении вниз, фигура становится частью ландшафта, т.е. не стирается с поля. С поля удаляются заполненные ряды, если такие есть, и выбирается следующая фигура.

За удаление заполненных рядов отвечает функция :removefulllines. Надеюсь, там ничего сложного.

Заключение

Мы убедились, что на языке bat-файлов вполне можно писать игры. В приведенном примере кода есть недостатки, зато я надеюсь, что он прост для понимания.

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

Мы продемонстрировали практическое использование псевдо-массивов, колбэков и базовых алгоритмов.

Комментарии (27)


  1. pae174
    09.03.2022 09:18
    +2

    Для перемещения курсора нам понадобится код ANSI-терминала <ESC>[1;1H. <ESC> – это символ с кодом 27 (033 или 0x1B).

    Не сработало на Win7 Eng Pro в cmd.exe. Стакан не перерисовывается. Вместо этого рисуются новые стаканы с этим вот кодом между ними.


    1. MAaxim91
      09.03.2022 09:28
      +2

      В Win10 выходило обновление, добавляющее поддержку ESC ANSI Sequences. Потому в более ранних осях это не работает.


      1. pae174
        09.03.2022 09:35

        Неа, в Win7 cmd.exe нормально отрабатывает Esc[1;1H если эти байты записаны в какой-нибудь test.txt и выдается команда cat test.txt.


        1. dmitryvolochaev Автор
          09.03.2022 09:36

          Не cat, а type, наверное


          1. pae174
            09.03.2022 09:43

            Ну да. У меня CygWin стоит - я по привычке...

            PS Нет, это зло! type как раз не работает. cat работает.


            1. ForNeVeR
              09.03.2022 09:53
              +7

              Вероятно, Cygwin'овский cat у вас как раз и обрабатывает ANSI-последовательности, преобразуя их к командам Windows console API.


    1. Dragokas
      09.03.2022 23:51
      +1

      Для получения ESC (а также BACKSPACE) не обязательно печатать спецсимвол напрямую. Можно получить их с помощью разбора prompt, см: https://www.cyberforum.ru/post4375870.html


    1. 586
      11.03.2022 22:07

      Для отображения Esc-последовательностей необходимо предварительно загружать драйвер ansi.com или ansi.sys.


      1. dmitryvolochaev Автор
        11.03.2022 22:09

        Не уверен, что в чистом досе и его command.com поддерживаются другие фичи, которые я тут использую


  1. unsignedchar
    09.03.2022 09:31
    +1

    Для таймаутов <1s можно использовать ping localhost ;)


    1. dmitryvolochaev Автор
      09.03.2022 09:32
      +2

      Можно. Но проблема в том, что нельзя проверить нажание клавиши без минимального таймаута


      1. unsignedchar
        09.03.2022 09:55

        Да. Штош, не заложили Отцы-Основатели такую функцию в MS-DOS.


  1. RumataEstora
    09.03.2022 11:15
    +3

    Посмотрите здесь: https://www.dostips.com/forum/viewtopic.php?f=3&t=6812

    Там используются сантисекундные задержки и ввод производится с помощью set /p и одного хитрого трюка (сам еще не до конца понял его) "%~F0" Input >> pipeFile.txt | "%~F0" Main < pipeFile.txt.


    1. dmitryvolochaev Автор
      09.03.2022 12:10
      +6

      Я понял, что там у них сделано.

      Для ввода используется xcopy /W. Опция /W означает ждать нажатия. При этом введенный символ выводится на stdout, а сообщение "Нажмите..." - на stderr. Важно передать в качестве источника и цели для копирования существующие файлы. Иначе xcopy спросит, файл или каталог указан как цель. В данном случае bat-файл копирует сам себя в себя же. Если вы до сих под четко знали азбучную истину о том, что нельзя копировать файл в себя, забудьте. xcopy так может.

      Для задержек используется цикл с проверкой, не достигла ли переменная %TIME% нужного значения. Из этой переменной берутся последние два символа - доли секунды. Перед ними добавляется единица, и действия производятся с числами от 100 до 199. Если единицу не добавлять, будет проблема с тем, что последовательность цифр, начинающаяся с нуля, означает число в 8-ричной системе. Т.е. 08 и 09 - это синтаксическая ошибка.

      А трюк для ввода без задержек заключается в том, что set /P в случае, когда ввод перенаправлен от другого процесса, не ждет ввода, как это обычно происходит. Если на вводе что-то есть, оно читается. Если ничего нет, переменная не устанавливается, но выполнение скрипта продолжается без задержек. Запускаются два процесса - %0 Input и %0 Main. Input с помощью xcopy ждет ввода, интерпретирует его и выводит код действия. Main использует set /P.

      Я, когда работал над статьей, рассматривал вариант с запуском процесса, который будет убивать choice.exe командой taskkill. Процесс, убитый таким образом, возвращает errorlevel 1. Надо сделать "s", т.е. движение вниз, не только вариантом по умолчанию, но еще и первым в списке и, казалось бы, всё.

      Но выяснилось, что много времени уходит на перерисовку поля и, возможно, на что-то еще. taskkill часто выдавал сообщение, что процесс не найден, и фигура опускалась не очень быстро, сколько бы я ни уменьшал задержку. Кроме того, надо как-то учесть, что в системе могут в это же время работать чужие экземпляры choice.exe, которые не надо трогать. Всё это заставило меня выложить статью без этого.


      1. AVX
        09.03.2022 12:48

        Раз уж всё равно используются внешние программы, то почему бы не попробовать использовать другой интерпретируемый язык, который позволяет сделать меньшие задержки? Например, можно в батник воткнуть кусок кода для powershell, и вызвать сам powershell, передав ему этот же батник (да, хитро можно разделить выполнение), а в powershell есть например start-sleep -m. Можно в цикле проверять нажатие клавиши и возвращать определённый код возврата. Хотя... тогда будет сильно проще и вовсе всё на powershell переписать, там возможностей больше.


      1. RumataEstora
        09.03.2022 12:53

        > Для ввода используется xcopy /W

        Теперь все понятно. Я видел эту команду, но потом забыл о ней и все не мог понять как set /p принимает ввод клавиш без ожидания.


  1. kovserg
    09.03.2022 11:16
    +3

    Прикольно конечно. А по что не в цвете если ANSI-последовательности поддерживаются?

    ps: ждём змейку


    1. twelve
      09.03.2022 18:58
      +2

      1. dmitryvolochaev Автор
        09.03.2022 19:00

        Всё-то уже сделали до нас


  1. magiavr
    09.03.2022 12:29
    +4

    "на bat-файлах не получится написать Doom". Легко :)

    cd Doom

    Doom.exe


  1. AlexanderS
    09.03.2022 14:21
    +4

    Поиграл… М-да… Несмотря на то, что я ещё тот любитель заскриптовать… оказывается я вовсе и не фанат, а так себе)


    1. dmitryvolochaev Автор
      09.03.2022 14:24

      Спасибо :)


  1. dmitryvolochaev Автор
    09.03.2022 14:23

    .


  1. IvanGo82
    09.03.2022 15:52

    Если кому интересно могу кинуть ссылку на свой js тетрис. там же собственно и код. что то порядка 150-200 строк


    1. dmitryvolochaev Автор
      09.03.2022 15:55

      JS - это в браузере? Или Node JS? Или, может, cscript/wscript?


      1. axe_chita
        10.03.2022 04:44

        В FreeDOS есть DOjS «JavaScript programming environment for systems running MS-DOS, FreeDOS or any DOS based Windows (like 95, 98, ME). It features an integrated editor, graphics & sound output, mouse/keyboard/joystick input and more. The latest DOjS release V1.7.0 includes a bunch of new features»


    1. LevOrdabesov
      10.03.2022 11:34

      CMD чудовищен в плане синтаксиса и практически полностью кастрирован в плане возможностей. С любым JS не сравнить.
      А тут ещё и на чистом CMD (я лично давно плюнул и стал юзать греп, сторонние парсеры и самописные утилиты, иначе боль).