Неделя 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 байт. Уверен, это не предел!
Ссылки
Кроме перечисленных выше, будут интересны: