Emoji Clicker
Emoji Clicker
В мире программирования создание небольших, но полноценных игр — это отличный способ отточить навыки и изучить новые технологии. Одним из таких проектов является Emoji Clicker — веб-игра, где игроки кликают на эмодзи, чтобы увеличивать счетчик. Несмотря на кажущуюся простоту, под капотом этой игры скрывается мощный симбиоз нескольких технологий: высокопроизводительного языка Julia для бэкенда, классического HTML/CSS/JavaScript для фронтенда и криптографических методов для безопасности данных.
Эта игра — больше чем просто кликер. Это пример современного подхода к разработке, где серверная логика, графика и взаимодействие с пользователем создаются и управляются из единой кодовой базы на Julia, что стирает границы между традиционным desktop-программированием и веб-разработкой.
Цель игры проста и затягивающая: нужно кликать на emoji в центре экрана. Каждый клик увеличивает счетчик. Чтобы сделать процесс визуально интересным, emoji меняется каждые 100 кликов, проходя целую эволюцию от улыбающегося смайлика 😀 до единорога 🦄. Игра также включает в себя полноценную систему сохранения прогресса, где ваши достижения (количество кликов, текущий аватар, время) шифруются в специальный ключ, который можно скопировать, а позже — ввести для продолжения игры с того же места.
Перейдём к реализации, нам понадобятся следующие библиотеки:
-
using Base64
— для кодирования и декодирования данных в формат Base64. Нужна, чтобы превращать бинарные данные (ваше сохранение) в текстовую строку, которую можно легко скопировать или ввести. -
using Dates
— для работы с датой и временем. Нужна, чтобы генерировать уникальный идентификатор на основе текущего времени и добавлять временную метку в сохранения.
using Base64, Dates
Теперь объявим ключевые константы проекта:
-
const EMOJIS
— это массив с эмодзи, которые будут использоваться в игре. Каждый элемент — это визуальный avatar игрока, который меняется по мере прогресса. -
const EMOJI_CHANGE_THRESHOLD = 100
— это порог смены эмодзи. Число 100 определяет, сколько кликов нужно набрать, чтобы текущий эмодзи сменился на следующий в массиве. Это создает систему вознаграждения и визуального прогресса, мотивируя пользователя кликать дальше.
const EMOJIS = ["😀", "😎", "🤩", "👨🚀", "🐘", "🙇♂️", "🐭", "🐆", "🐵", "🦄"]
const EMOJI_CHANGE_THRESHOLD = 100
Функция encrypt_data
— это кастомный шифратор для защиты данных сохранения игры. он принимает исходные данные (data
) в виде строки и числовой ключ (key
) по умолчанию 0x55
, после чего функция обрабатывает каждый байт строки: XOR (⊻
): Накладывает на байт ключ с помощью операции "исключающее ИЛИ", битовый сдвиг: "Перемешивает" биты зашифрованного байта, сдвигая их на 2 позиции влево (<< 2
) и объединяя с результатом сдвига на 6 позиций вправо (>> 6
). Далее функция кодирует получившийся набор байтов и преобразует их в текстовую строку в кодировке Base64, чтобы её можно было легко скопировать или ввести.
function encrypt_data(data::String, key::UInt8=0x55)::String
encrypted = Vector{UInt8}()
for byte in Vector{UInt8}(data)
encrypted_byte = (byte ⊻ key) << 2 | (byte ⊻ key) >> 6
push!(encrypted, encrypted_byte)
end
return base64encode(encrypted)
end
decrypt_data
— это обратная функция для encrypt_data
. Она расшифровывает данные, алгоритм коротко описан ниже:
- Декодирует строку из Base64 обратно в байты.
- Для каждого байта выполняет обратные битовые сдвиги:
(byte >> 2) | (byte << 6)
. - Применяет операцию XOR с тем же ключом (
0x55
), чтобы вернуть исходный байт. - Собирает все байты в строку и возвращает её.
function decrypt_data(encrypted_data::String, key::UInt8=0x55)::String
decoded = base64decode(encrypted_data)
decrypted = Vector{UInt8}()
for byte in decoded
decrypted_byte = (byte >> 2) | (byte << 6)
push!(decrypted, decrypted_byte ⊻ key)
end
return String(decrypted)
end
Функция create_clicker_interface
является центральным элементом всего приложения. Её главная задача — динамически сгенерировать и отобразить в браузере полноценный игровой интерфейс. Для этого она создает большую строку, содержащую весь необходимый HTML, CSS и JavaScript код. Чтобы гарантировать, что несколько экземпляров игры не будут конфликтовать между собой, функция генерирует уникальный идентификатор, который встраивается в имена всех элементов и функций. Это обеспечивает изоляцию и корректную работу логики.
Встроенный JavaScript код отвечает за всю интерактивность на стороне клиента. Функция handleClick
обрабатывает каждое нажатие на эмодзи: увеличивает счетчик, воспроизводит анимацию легкого сжатия и проверяет, не пришло ли время сменить аватар игрока на следующий, исходя из достигнутого порога кликов. Это и есть игровая механика в чистом виде.
Система сохранений реализована через выпадающее меню, управляемое функцией toggleDropdown
. Функция generateKey
собирает все данные игрока (текущий счет, индекс эмодзи и временную метку), формирует из них строку и кодирует её в Base64, создавая тот самый "ключ сохранения". Функция loadKey
выполняет обратную операцию: она декодирует введенный пользователем ключ, извлекает из него данные и восстанавливает состояние игры, позволяя продолжить с прерванного места.
function create_clicker_interface()
println("🎮 Запуск Emoji Clicker!")
println("👇 Кликайте на эмоджи в центре!")
unique_id = string(hash(now()), rand(1000:9999))
html_interface = """
<div style="font-family: 'Arial', sans-serif; text-align: center; padding: 40px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; color: white;">
<h1 style="font-size: 3em; margin-bottom: 30px; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">
✨ Emoji Clicker ✨
</h1>
<div id="counter_$(unique_id)"
style="font-size: 4em; font-weight: bold; margin: 30px 0;
background: rgba(255,255,255,0.1); padding: 20px;
border-radius: 15px; display: inline-block; min-width: 200px;">
0
</div>
<div id="emoji_$(unique_id)"
style="font-size: 8em; cursor: pointer; margin: 40px 0;
transition: all 0.2s ease; user-select: none;
text-shadow: 4px 4px 8px rgba(0,0,0,0.4);
animation: float_$(unique_id) 3s ease-in-out infinite;">
$(EMOJIS[1])
</div>
<div style="position: relative; display: inline-block;">
<button id="saveBtn_$(unique_id)"
style="background: rgba(255,255,255,0.2); border: 3px solid rgba(255,255,255,0.5);
color: white; padding: 15px 30px; border-radius: 50px;
cursor: pointer; font-size: 1.2em; margin: 20px 0;
transition: all 0.3s ease; font-weight: bold;">
💾 Сохранения
</button>
<div id="saveDropdown_$(unique_id)"
style="display: none; position: absolute; top: 100%; left: 0;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
padding: 20px; border-radius: 15px; margin-top: 10px;
min-width: 300px; z-index: 1000; box-shadow: 0 8px 25px rgba(0,0,0,0.3);">
<h3 style="margin-bottom: 15px; color: white;">💾 Управление сохранениями</h3>
<div style="margin-bottom: 15px;">
<button onclick="generateKey_$(unique_id)()"
style="background: #27ae60; color: white; border: none;
padding: 10px 20px; border-radius: 8px; cursor: pointer;
font-size: 0.9em; font-weight: bold; width: 100%;">
🔑 Создать новое сохранение
</button>
</div>
<div id="keyDisplay_$(unique_id)"
style="background: rgba(255,255,255,0.1); padding: 12px;
border-radius: 10px; margin-bottom: 15px; display: none;">
<div style="font-size: 0.9em; margin-bottom: 8px;">Ключ сохранения:</div>
<div id="keyValue_$(unique_id)" style="word-break: break-all; font-family: monospace; font-size: 0.8em;"></div>
<button onclick="copyKey_$(unique_id)()"
style="background: #3498db; color: white; border: none;
padding: 6px 12px; border-radius: 5px; cursor: pointer;
margin-top: 8px; font-size: 0.8em;">
📋 Копировать
</button>
</div>
<div style="margin-bottom: 15px;">
<input type="text" id="loadKeyInput_$(unique_id)"
placeholder="Введите ключ сохранения"
style="padding: 10px; border-radius: 8px; border: none;
width: 100%; margin-bottom: 10px; font-size: 0.9em;">
<button onclick="loadKey_$(unique_id)()"
style="background: #e67e22; color: white; border: none;
padding: 10px 20px; border-radius: 8px; cursor: pointer;
font-size: 0.9em; font-weight: bold; width: 100%;">
🎮 Загрузить сохранение
</button>
</div>
<div id="loadResult_$(unique_id)"
style="background: rgba(255,255,255,0.1); padding: 12px;
border-radius: 10px; margin-bottom: 15px; display: none;">
<div style="font-size: 0.9em; margin-bottom: 8px;">Данные сохранения:</div>
<div id="decryptedData_$(unique_id)" style="word-break: break-all; font-size: 0.8em;"></div>
</div>
</div>
</div>
<style>
@keyframes float_$(unique_id) {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
</style>
</div>
<script>
let clickCount_$(unique_id) = 0;
let dropdownVisible_$(unique_id) = false;
const emojis_$(unique_id) = $(EMOJIS);
const threshold_$(unique_id) = $(EMOJI_CHANGE_THRESHOLD);
function handleClick_$(unique_id)() {
clickCount_$(unique_id)++;
const counterElement = document.getElementById('counter_$(unique_id)');
counterElement.textContent = clickCount_$(unique_id);
const emojiElement = document.getElementById('emoji_$(unique_id)');
emojiElement.style.transform = 'scale(0.8)';
setTimeout(() => {
emojiElement.style.transform = 'scale(1)';
}, 100);
if (clickCount_$(unique_id) % threshold_$(unique_id) === 0) {
const emojiIndex = Math.min(
Math.floor(clickCount_$(unique_id) / threshold_$(unique_id)) % emojis_$(unique_id).length,
emojis_$(unique_id).length - 1
);
emojiElement.textContent = emojis_$(unique_id)[emojiIndex];
}
}
function toggleDropdown_$(unique_id)() {
dropdownVisible_$(unique_id) = !dropdownVisible_$(unique_id);
document.getElementById('saveDropdown_$(unique_id)').style.display = dropdownVisible_$(unique_id) ? 'block' : 'none';
}
function generateKey_$(unique_id)() {
const timestamp = new Date().toISOString();
const emojiIndex = Math.min(Math.floor(clickCount_$(unique_id) / threshold_$(unique_id)) % emojis_$(unique_id).length, emojis_$(unique_id).length - 1);
const data = "Count:" + clickCount_$(unique_id) + "|Time:" + timestamp + "|Emoji:" + emojiIndex;
const keyDisplay = document.getElementById('keyDisplay_$(unique_id)');
const keyValue = document.getElementById('keyValue_$(unique_id)');
keyValue.textContent = btoa(data);
keyDisplay.style.display = 'block';
}
function copyKey_$(unique_id)() {
const keyText = document.getElementById('keyValue_$(unique_id)').textContent;
navigator.clipboard.writeText(keyText).then(() => {
alert('Ключ скопирован!');
});
}
function loadKey_$(unique_id)() {
const keyInput = document.getElementById('loadKeyInput_$(unique_id)').value;
const resultDiv = document.getElementById('loadResult_$(unique_id)');
const decryptedDiv = document.getElementById('decryptedData_$(unique_id)');
try {
const decodedData = atob(keyInput);
decryptedDiv.textContent = decodedData;
resultDiv.style.display = 'block';
const countMatch = decodedData.match(/Count:(\\d+)/);
const emojiMatch = decodedData.match(/Emoji:(\\d+)/);
if (countMatch && emojiMatch) {
clickCount_$(unique_id) = parseInt(countMatch[1]);
const emojiIndex = parseInt(emojiMatch[1]);
document.getElementById('counter_$(unique_id)').textContent = clickCount_$(unique_id);
document.getElementById('emoji_$(unique_id)').textContent = emojis_$(unique_id)[emojiIndex];
alert('Сохранение успешно загружено!');
document.getElementById('loadKeyInput_$(unique_id)').value = '';
} else {
alert('Ошибка: неверный формат данных в ключе');
}
} catch (e) {
decryptedDiv.textContent = 'Ошибка: неверный формат ключа';
resultDiv.style.display = 'block';
alert('Ошибка при загрузке сохранения: неверный формат ключа');
}
}
document.getElementById('emoji_$(unique_id)').onclick = handleClick_$(unique_id);
document.getElementById('saveBtn_$(unique_id)').onclick = toggleDropdown_$(unique_id);
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('saveDropdown_$(unique_id)');
const button = document.getElementById('saveBtn_$(unique_id)');
if (dropdownVisible_$(unique_id) && !dropdown.contains(event.target) && !button.contains(event.target)) {
dropdownVisible_$(unique_id) = false;
dropdown.style.display = 'none';
}
});
</script>
"""
display("text/html", html_interface)
end
create_clicker_interface()
Вывод
В заключение можно сказать, что данный проект представляет собой удачный синтез высокопроизводительных вычислений на Julia и современных веб-технологий, реализующий полноценную игровую механику в интерактивном веб-интерфейсе. Ключевой особенностью является архитектурное решение, при котором весь комплекс — от бэкенд-логики и криптографических функций до генерации визуально привлекательного фронтенда — инкапсулирован в единую кодовую базу, что демонстрирует его универсальность не только в научной сфере, но и в области создания практических веб-приложений с элементами игрового дизайна и защиты данных.