«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) { поражение; } Жмём На этом основная работа закончена. Добавляем вёрстку, оборачиваем приложение в анонимную функцию и проверяем в нескольких браузерах.Результат до сжатия
press up! Прогоняем JS через любой оптимизатор а-ля UglifyJS после чего просто переносим его в: