Сообщество Engee

Emoji Clicker

Автор
avatar-yurevyurev
Notebook

Emoji Clicker

В мире программирования создание небольших, но полноценных игр — это отличный способ отточить навыки и изучить новые технологии. Одним из таких проектов является Emoji Clicker — веб-игра, где игроки кликают на эмодзи, чтобы увеличивать счетчик. Несмотря на кажущуюся простоту, под капотом этой игры скрывается мощный симбиоз нескольких технологий: высокопроизводительного языка Julia для бэкенда, классического HTML/CSS/JavaScript для фронтенда и криптографических методов для безопасности данных.

Эта игра — больше чем просто кликер. Это пример современного подхода к разработке, где серверная логика, графика и взаимодействие с пользователем создаются и управляются из единой кодовой базы на Julia, что стирает границы между традиционным desktop-программированием и веб-разработкой.

Цель игры проста и затягивающая: нужно кликать на emoji в центре экрана. Каждый клик увеличивает счетчик. Чтобы сделать процесс визуально интересным, emoji меняется каждые 100 кликов, проходя целую эволюцию от улыбающегося смайлика 😀 до единорога 🦄. Игра также включает в себя полноценную систему сохранения прогресса, где ваши достижения (количество кликов, текущий аватар, время) шифруются в специальный ключ, который можно скопировать, а позже — ввести для продолжения игры с того же места.

Перейдём к реализации, нам понадобятся следующие библиотеки:

  • using Base64 — для кодирования и декодирования данных в формат Base64. Нужна, чтобы превращать бинарные данные (ваше сохранение) в текстовую строку, которую можно легко скопировать или ввести.

  • using Dates — для работы с датой и временем. Нужна, чтобы генерировать уникальный идентификатор на основе текущего времени и добавлять временную метку в сохранения.

In [ ]:
using Base64, Dates

Теперь объявим ключевые константы проекта:

  • const EMOJIS — это массив с эмодзи, которые будут использоваться в игре. Каждый элемент — это визуальный avatar игрока, который меняется по мере прогресса.

  • const EMOJI_CHANGE_THRESHOLD = 100 — это порог смены эмодзи. Число 100 определяет, сколько кликов нужно набрать, чтобы текущий эмодзи сменился на следующий в массиве. Это создает систему вознаграждения и визуального прогресса, мотивируя пользователя кликать дальше.

In [ ]:
const EMOJIS = ["😀", "😎", "🤩", "👨‍🚀", "🐘", "🙇‍♂️", "🐭", "🐆", "🐵", "🦄"]
const EMOJI_CHANGE_THRESHOLD = 100
WARNING: redefinition of constant Main.EMOJIS. This may fail, cause incorrect answers, or produce other errors.
Out[0]:
100

Функция encrypt_data — это кастомный шифратор для защиты данных сохранения игры. он принимает исходные данные (data) в виде строки и числовой ключ (key) по умолчанию 0x55, после чего функция обрабатывает каждый байт строки: XOR (): Накладывает на байт ключ с помощью операции "исключающее ИЛИ", битовый сдвиг: "Перемешивает" биты зашифрованного байта, сдвигая их на 2 позиции влево (<< 2) и объединяя с результатом сдвига на 6 позиций вправо (>> 6). Далее функция кодирует получившийся набор байтов и преобразует их в текстовую строку в кодировке Base64, чтобы её можно было легко скопировать или ввести.

In [ ]:
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
Out[0]:
encrypt_data (generic function with 2 methods)

decrypt_data — это обратная функция для encrypt_data. Она расшифровывает данные, алгоритм коротко описан ниже:

  1. Декодирует строку из Base64 обратно в байты.
  2. Для каждого байта выполняет обратные битовые сдвиги: (byte >> 2) | (byte << 6).
  3. Применяет операцию XOR с тем же ключом (0x55), чтобы вернуть исходный байт.
  4. Собирает все байты в строку и возвращает её.
In [ ]:
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
Out[0]:
decrypt_data (generic function with 2 methods)

Функция create_clicker_interface является центральным элементом всего приложения. Её главная задача — динамически сгенерировать и отобразить в браузере полноценный игровой интерфейс. Для этого она создает большую строку, содержащую весь необходимый HTML, CSS и JavaScript код. Чтобы гарантировать, что несколько экземпляров игры не будут конфликтовать между собой, функция генерирует уникальный идентификатор, который встраивается в имена всех элементов и функций. Это обеспечивает изоляцию и корректную работу логики.

Встроенный JavaScript код отвечает за всю интерактивность на стороне клиента. Функция handleClick обрабатывает каждое нажатие на эмодзи: увеличивает счетчик, воспроизводит анимацию легкого сжатия и проверяет, не пришло ли время сменить аватар игрока на следующий, исходя из достигнутого порога кликов. Это и есть игровая механика в чистом виде.

Система сохранений реализована через выпадающее меню, управляемое функцией toggleDropdown. Функция generateKey собирает все данные игрока (текущий счет, индекс эмодзи и временную метку), формирует из них строку и кодирует её в Base64, создавая тот самый "ключ сохранения". Функция loadKey выполняет обратную операцию: она декодирует введенный пользователем ключ, извлекает из него данные и восстанавливает состояние игры, позволяя продолжить с прерванного места.

In [ ]:
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()
🎮 Запуск Emoji Clicker!
👇 Кликайте на эмоджи в центре!

✨ Emoji Clicker ✨

0
😀

Вывод

В заключение можно сказать, что данный проект представляет собой удачный синтез высокопроизводительных вычислений на Julia и современных веб-технологий, реализующий полноценную игровую механику в интерактивном веб-интерфейсе. Ключевой особенностью является архитектурное решение, при котором весь комплекс — от бэкенд-логики и криптографических функций до генерации визуально привлекательного фронтенда — инкапсулирован в единую кодовую базу, что демонстрирует его универсальность не только в научной сфере, но и в области создания практических веб-приложений с элементами игрового дизайна и защиты данных.