«Flappy Bird» до 1КБ

    Неделя 30-ти строчных JS давно прошла, но воодушевлённый постами Разрабатываем Flappy Bird на Phaser (Часть I) и Как Минковский во Flappy Bird играл, я не смог удержаться не попробовать написать ASCII-версию игры «Flappy Bird» на JavaScript и уложиться при этом в 1024 символа.

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

    Об игре


    Я опущу скучные подробности, которые вы и сами можете найти в вики или попробовав сыграть в оригинальную игру. Вкратце же вы управляете птичкой, летящей между препятствиями. Клавишей «вверх» можно слегка кратковременно подтолкнуть её выше и таким образом маневрировать.

    Поле


    Я не стремился к достаточной аутентичности, да и ограничения весьма жёсткие. Поэтому пошёл на максимальные жертвы и, методом проб и ошибок, подобрал более менее оптимальный размер поля (без масштабирования) — 10 строк по 30 символов в каждой:
    -+++++----+++++-----+++++-----
     +++++    +++++     +++++     
     +++++    +++++     +++++     
     +++++                        
                                  
     0                            
              +++++     +++++     
     +++++    +++++     +++++     
     +++++    +++++     +++++     
    --++++----+++++-----+++++-----
    123
    

    Нолик — позиция игрока (я выбрал второй слева столбец), 123 внизу слева — набранные очки.

    Анимация поля


    Первая задача — научиться двигать игровое поле, смещать препятствия справа налево. Можно попробовать хранить ячейки поля в двумерном массиве, но мы пойдём другим путём. Давайте рассмотрим одну из строк, заменив в ней пробелы на нули, а преграду на единички:
    011111000011111000001111100000
    Не трудно догадаться, что это двоичное представление числа (521110496). Тем проще для нас — двинуть его влево можно побитовой операцией сдвига влево. Помним про предел длины целочисленных значений. Для сохранения ограничения в 30 байт, просто маскируем их, отрезая всё лишнее после сдвига:
    строка = 011111000011111000001111100000;
    строка = строка << 1;
    строка = строка & 2^30;
    // строка = 111110000111110000011111000000
    

    И вторая задача — сохранить одинаковую ширину препятствий. Четыре возможных случая (до первого ложного) выглядят так:
    ????010 - true -> 000011
    ???0110 - true -> 000111
    ??01110 - true -> 001111
    ?011110 - true -> 011111
    0111110 - false
    

    Логика тривиальна: если второй бит = 1, то истина в случае, если есть бит = 0 в следующих за вторым битах до ширины препятствия.
    Итого для сдвига всех строк поля применяем:
    строка <<= 1;
    строка &= 2^30;
    if (строка & 2) {
        for (бит = 2; бит <= ширина_препятствия; бит++) {
            if (!(строка & 1 << бит)) {
                строка |= 1;
                break;
            }
        }
    }
    


    Подсчёт очков


    В этом месте у нас достаточно условий для подсчёта очков. При условии, что два препятствия не могут идти без интервала, мы добавляем балл за каждый пустой столбец поля, в котором находится игрок, если в предыдущем столбце препятствие точно было (28 и 29 — индексы двух соседних столбцов в одном из которых, с меньшим индексом, — игрок):
    очки += (первая_строка_поля & 1 << 29) && !(первая_строка_поля & 1 << 28) ? 1 : 0;
    


    Новые препятствия


    С этим немного сложнее. Я попробовал выдержать условия:
    • препятствия не должны идти подряд без промежутков
    • чем больше промежуток уже образовался, тем выше должна быть вероятность появления препятствия
    • чем больше игрок набрал очков, тем чаще должны идти препятствия

    Наглядно это можно представить в таком виде:
    000011111 ->     0% (0 * 12.5) 0
    000111110 ->     0% (0 * 12.5) 1
    001111100 ->  12.5% (1 * 12.5) 2
    011111000 ->  25.0% (2 * 12.5) 3
    111110000 ->  37.5% (3 * 12.5) ..
    111100000 ->  50.0% (4 * 12.5)
    111000000 ->  62.5% (5 * 12.5)
    110000000 ->  75.0% (6 * 12.5)
    100000000 ->  87.5% (7 * 12.5)
    000000000 -> 100.0% (8 * 12.5)
    

    Первый столбик — крайние биты поля, далее идут проценты вероятностей появления нового препятствия. Восемь — максимальный интервал, который и оптимален в игре, и удобен для расчётов: 100 можно поделить на 8 и получить осязаемые значения. Крайний справа столбец — величина побитового сдвига влево той маски, которой мы будем искать и вычислять длину текущего промежутка между препятствиями.
    Дело за малым: двигать побитово единицу-маску влево, пока не встретим ещё одну единицу. В этот момент, зная текущий промежуток и вероятность, пытаемся создать новое препятствие:
    для каждой попытки от 0 до бесконечности {
        if (первая_строка_поля & 1 << попытка) {
            if (попытка > 1 && (попытка - 2) * 125 + очки > случайное(124..999)) {
                // создаём препятствие
            }
            break;
        }
    }
    

    Я умножил всё на 10 и таким образом избавился от нецелых значений. Кроме 100%: от тысячи (100%*10) я отнял единицу, потому что единица — это ж целый лишний байт приложения! А, как мы помним, байты надо экономить.
    Добавление самих препятствий задача не сложная, но для исключения создания непроходимых участков, я добавил условие: каждое следующее препятствие должно быть на единицу больше/меньше предыдущего или равно ему, и при этом не быть меньше двух или больше пяти. Плюс выдерживаем промежуток для полёта — три строчки. Получаем:
    // ВВП - высота верхнего препятствия, а не то, что вы подумали
    ВВП = случайное(
        от ВВП > 2 ? ВВП - 1 : 2
        до ВВП < 5 ? ВВП + 1 : 5
    );
    для строк от 0 до ВВП {
        строка |= 1;
    }
    для строк от ВВП + 3 до последней {
        строка |= 1;
    }
    


    Рендер


    Тут никаких ухищрений. Просто бежим, как старый ламповый телевизор, по строкам, а в них по столбцам и накапливаем клетки поля:
    поле = '';
    для всех строк (сверху вниз) {
        для всех столбцов (слева направо) {
            поле += столбец == 28 && строка == позиция_игрок ? "0" : (
                строка & 1 << столбец ? "+" : (
                    !строка || строка == всего_строк - 1 ? "-" : " "
                )
            );
        }
        поле += "\n";
    }
    обновляем_поле;
    

    Отдельно проверяем и рисуем игрока, а также верхнюю и нижнюю границу самого поля.

    Ход


    Игровой мир готов и работает. Осталось оживить персонажа. Я упростил это по максимуму. Никаких полётов по параболе, гравитации, ускорений и инерции (разве что самую малость). Пусть клавиша «вверх» задаёт импульс — запас движения вверх. И пусть каждая итерация анимации уменьшает этот импульс, если он ещё не достиг нуля. На тестах это выглядело совсем убого и птичка двигалась по явно треугольной траектории. Поэтому я немного увеличил начальный импульс и добавил движение «по инерции», если импульс равен единице:
    if (нажата клавиша вверх) {
        запускаем игру, либо импульс = 3;
    }
    
    // вверх
    if (импульс > 1) {
        импульс--;
        --позиция_персонажа || поражение;
    // инерция
    } else if (импульс) {
        импульс--;
    // вниз
    } else {
        позиция_персонажа < 9 ? позиция_персонажа++ : поражение;
    }
    

    Импульс не только управляет своим значением, но и позицией персонажа. Плюс проверяет, не ударились ли мы в границы поля.
    Отдельно проверяем столкновение с препятствием:
    if (строка_персонажа & 1 << 28) {
        поражение;
    }
    


    Жмём


    На этом основная работа закончена. Добавляем вёрстку, оборачиваем приложение в анонимную функцию и проверяем в нескольких браузерах.
    Результат до сжатия
    <script>
    (function(){
        var run = 0, imp = 0;
        function up(){
            run = 1;
    
            var
            pos = 2,
            rows = [1, 1, 1, 1, 0, 0, 0, 1, 1, 1],
            rowsLen = 10,
            fieldWidth = 30,
            fieldMask = Math.pow(2, fieldWidth) - 1,
            profit = 0,
            hTop = 4,
            row,
            col,
            timer = setInterval(function(){
                /**
                 * Move user
                 */
                if (imp > 1) {
                    imp--;
                    // up
                    --pos || _stop();
                } else if (imp) {
                    imp--;
                } else {
                    // down
                    pos < 9 ? pos++ : _stop();
                }
    
    
                /**
                 * Move field
                 *
                 * 0111110 - false
                 * ?011110 - true -> 011111
                 * ??01110 - true -> 001111
                 * ???0110 - true -> 000111
                 * ????010 - true -> 000011
                 */
                for (row = rowsLen; row--;) {
                    rows[row] <<= 1;
                    rows[row] &= fieldMask;
                    if (rows[row] & 2) {
                        for (w = 2; w <= 5; w++) {
                            if (!(rows[row] & 1 << w)) {
                                rows[row] |= 1;
                                break;
                            }
                        }
                    }
                }
    
    
                /**
                 * Add new objects
                 *
                 *
                 * 000011111 ->     0% (0 * 12.5) 0
                 * 000111110 ->     0% (0 * 12.5) 1
                 * 001111100 ->  12.5% (1 * 12.5) 2
                 * 011111000 ->  25.0% (2 * 12.5) 3
                 * 111110000 ->  37.5% (3 * 12.5) ..
                 * 111100000 ->  50.0% (4 * 12.5)
                 * 111000000 ->  62.5% (5 * 12.5)
                 * 110000000 ->  75.0% (6 * 12.5)
                 * 100000000 ->  87.5% (7 * 12.5)
                 * 000000000 -> 100.0% (8 * 12.5)
                 */
                for (var tryNum = 0; true; tryNum++) {
                    if (rows[0] & 1 << tryNum) {
                        if (tryNum > 1 && (tryNum - 2) * 125 + profit > _rnd(124, 999)) {
                            hTop = _rnd(hTop > 2 ? hTop - 1 : 2, hTop < 5 ? hTop + 1 : 5); // 2..5, prev +/- 1
                            for (h = 0; h < hTop; h++) {
                                rows[h] |= 1;
                            }
                            for (h = hTop + 3; h < rowsLen; h++) {
                                rows[h] |= 1;
                            }
                        }
                        break;
                    }
                }
    
                /**
                 * Render
                 */
                var text = '';
                for (row = 0; row < rowsLen; row++) {
                    for (col = 29; col >= 0; col--) {
                        text += col == 28 && row == pos ? "0" : (
                            rows[row] & 1 << col ? "+" : (
                                !row || row == rowsLen - 1 ? "-" : " "
                            )
                        );
                    }
                    text += "\n";
                }
                profit += (rows[0] & 1 << 29) && !(rows[0] & 1 << 28) ? 1 : 0;
                text += "\n"+profit;
                pre.innerHTML = text;
    
                if (rows[pos] & 1 << 28) {
                    _stop();
                }
            }, 250);
    
            var _rnd = function(min, max){
                return Math.floor(Math.random() * (max - min + 1)) + min;
            }
            var _stop = function(){
                clearInterval(timer);
                run && alert(':(');
                run = 0;
            }
        }
        onkeyup = function(e){ e.which == 38 && (run ? imp = 3 : up()); };
    })()
    </script>
    <body onload=""><pre id="pre">press up!
    


    Прогоняем JS через любой оптимизатор а-ля UglifyJS после чего просто переносим его в:
    <body onload='сюда..'
    

    Итого: 785 байт. Уверен, это не предел!

    Ссылки


    Кроме перечисленных выше, будут интересны:
    • +29
    • 20.4k
    • 9
    Share post

    Comments 9

      +31
      Что-то ФПС низкий, видеокарта игру не тянет, наверное
        +3
        Можете в оставшиеся до 1024 байты добавить поддержку тачей.
          +5
          Оригинальная посложнее будет.
            +1
            Никогда не переводите языковые конструкции на русский, выглядит ужасно и читается хуже. Лучше комментариями код снабжать.
              +1
              Эх, вы пропустили самую интересную часть — ручную минификацию.

              По крайней мере, что бросается в глаза
              — Оборачивание в IIFE можно смело вырезать.

              f=30,g=Math.pow(2,f)-1 круто, но (1<<30)-1 короче.

              Math.floor(n) для положительных чисел можно заменить n|0 или ~~n

              for(w=2;5>=w;w++) то же самое, что for(w=1;5>w++;)

              var a=0,b=0 можно сократить до a=b=0 (да, убрав var)
                0
                Merged. Спасибо!
                0
                Осталось сваять FlappyBird на Brainfuck, чтобы, так сказать, форма соответствовала содержанию.
                  +1
                  Для тех, кому довелось играть в «Боа» на Спектруме (или, как мне, на БК0011) demo (github). Пока не смог сделать лучше, чем 1022 байта.
                    0
                    И совсем для олдскульных геймеров — Xonix github.com/Lexx918/JS.Xonix
                    В этот раз 2Кб. Меньше — совсем тяжко. Чуток не дотянул до полной аутентичности 1984 года.

                  Only users with full accounts can post comments. Log in, please.