Неделя 30-ти строчных JS давно прошла, но воодушевлённый постами Разрабатываем Flappy Bird на Phaser (Часть I) и Как Минковский во Flappy Bird играл, я не смог удержаться не попробовать написать ASCII-версию игры «Flappy Bird» на JavaScript и уложиться при этом в 1024 символа.
Посмотреть, что из этого вышло (и поиграть) можно тут, а несжатый исходник увидеть здесь.
За деталями реализации прошу под кат.
Я опущу скучные подробности, которые вы и сами можете найти в вики или попробовав сыграть в оригинальную игру. Вкратце же вы управляете птичкой, летящей между препятствиями. Клавишей «вверх» можно слегка кратковременно подтолкнуть её выше и таким образом маневрировать.
Я не стремился к достаточной аутентичности, да и ограничения весьма жёсткие. Поэтому пошёл на максимальные жертвы и, методом проб и ошибок, подобрал более менее оптимальный размер поля (без масштабирования) — 10 строк по 30 символов в каждой:
Нолик — позиция игрока (я выбрал второй слева столбец), 123 внизу слева — набранные очки.
Первая задача — научиться двигать игровое поле, смещать препятствия справа налево. Можно попробовать хранить ячейки поля в двумерном массиве, но мы пойдём другим путём. Давайте рассмотрим одну из строк, заменив в ней пробелы на нули, а преграду на единички:
011111000011111000001111100000
Не трудно догадаться, что это двоичное представление числа (521110496). Тем проще для нас — двинуть его влево можно побитовой операцией сдвига влево. Помним про предел длины целочисленных значений. Для сохранения ограничения в 30 байт, просто маскируем их, отрезая всё лишнее после сдвига:
И вторая задача — сохранить одинаковую ширину препятствий. Четыре возможных случая (до первого ложного) выглядят так:
Логика тривиальна: если второй бит = 1, то истина в случае, если есть бит = 0 в следующих за вторым битах до ширины препятствия.
Итого для сдвига всех строк поля применяем:
В этом месте у нас достаточно условий для подсчёта очков. При условии, что два препятствия не могут идти без интервала, мы добавляем балл за каждый пустой столбец поля, в котором находится игрок, если в предыдущем столбце препятствие точно было (28 и 29 — индексы двух соседних столбцов в одном из которых, с меньшим индексом, — игрок):
С этим немного сложнее. Я попробовал выдержать условия:
Наглядно это можно представить в таком виде:
Первый столбик — крайние биты поля, далее идут проценты вероятностей появления нового препятствия. Восемь — максимальный интервал, который и оптимален в игре, и удобен для расчётов: 100 можно поделить на 8 и получить осязаемые значения. Крайний справа столбец — величина побитового сдвига влево той маски, которой мы будем искать и вычислять длину текущего промежутка между препятствиями.
Дело за малым: двигать побитово единицу-маску влево, пока не встретим ещё одну единицу. В этот момент, зная текущий промежуток и вероятность, пытаемся создать новое препятствие:
Я умножил всё на 10 и таким образом избавился от нецелых значений. Кроме 100%: от тысячи (100%*10) я отнял единицу, потому что единица — это ж целый лишний байт приложения! А, как мы помним, байты надо экономить.
Добавление самих препятствий задача не сложная, но для исключения создания непроходимых участков, я добавил условие: каждое следующее препятствие должно быть на единицу больше/меньше предыдущего или равно ему, и при этом не быть меньше двух или больше пяти. Плюс выдерживаем промежуток для полёта — три строчки. Получаем:
Тут никаких ухищрений. Просто бежим, как старый ламповый телевизор, по строкам, а в них по столбцам и накапливаем клетки поля:
Отдельно проверяем и рисуем игрока, а также верхнюю и нижнюю границу самого поля.
Игровой мир готов и работает. Осталось оживить персонажа. Я упростил это по максимуму. Никаких полётов по параболе, гравитации, ускорений и инерции (разве что самую малость). Пусть клавиша «вверх» задаёт импульс — запас движения вверх. И пусть каждая итерация анимации уменьшает этот импульс, если он ещё не достиг нуля. На тестах это выглядело совсем убого и птичка двигалась по явно треугольной траектории. Поэтому я немного увеличил начальный импульс и добавил движение «по инерции», если импульс равен единице:
Импульс не только управляет своим значением, но и позицией персонажа. Плюс проверяет, не ударились ли мы в границы поля.
Отдельно проверяем столкновение с препятствием:
На этом основная работа закончена. Добавляем вёрстку, оборачиваем приложение в анонимную функцию и проверяем в нескольких браузерах.
Прогоняем JS через любой оптимизатор а-ля UglifyJS после чего просто переносим его в:
Итого: 785 байт. Уверен, это не предел!
Кроме перечисленных выше, будут интересны:
Посмотреть, что из этого вышло (и поиграть) можно тут, а несжатый исходник увидеть здесь.
За деталями реализации прошу под кат.
Об игре
Я опущу скучные подробности, которые вы и сами можете найти в вики или попробовав сыграть в оригинальную игру. Вкратце же вы управляете птичкой, летящей между препятствиями. Клавишей «вверх» можно слегка кратковременно подтолкнуть её выше и таким образом маневрировать.
Поле
Я не стремился к достаточной аутентичности, да и ограничения весьма жёсткие. Поэтому пошёл на максимальные жертвы и, методом проб и ошибок, подобрал более менее оптимальный размер поля (без масштабирования) — 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 байт. Уверен, это не предел!
Ссылки
Кроме перечисленных выше, будут интересны:
