Engee documentation
Notebook

Emoji Clicker

In the world of programming, creating small but full—fledged games is a great way to hone your skills and explore new technologies. One of these projects is Emoji Clicker, a web game where players click on emojis to increase the counter. Despite its apparent simplicity, there is a powerful symbiosis of several technologies under the hood of this game: the high-performance Julia language for the backend, classic HTML/CSS/JavaScript for the front-end, and cryptographic methods for data security.

This game is more than just a clicker. This is an example of a modern development approach where server logic, graphics, and user interaction are created and managed from a single Julia codebase, blurring the boundaries between traditional desktop programming and web development.

The goal of the game is simple and addictive: you need to click on the emoji in the center of the screen. Each click increases the counter. To make the process visually interesting, emoji changes every 100 clicks, going through a whole evolution from a smiling smiley face to a unicorn. The game also includes a full—fledged progress saving system, where your achievements (number of clicks, current avatar, time) are encrypted into a special key that can be copied and later entered to continue the game from the same place.

Let's move on to the implementation, we will need the following libraries:

  • using Base64 — for encoding and decoding data in the Base64 format. It is needed to turn binary data (your save) into a text string that can be easily copied or entered.

  • using Dates — for working with date and time. It is needed to generate a unique identifier based on the current time and add a timestamp to the saves.

In [ ]:
using Base64, Dates

Now let's declare the key constants of the project:

  • const EMOJIS is an array of emojis that will be used in the game. Each element is a visual avatar of the player, which changes as you progress.

  • const EMOJI_CHANGE_THRESHOLD = 100 — this is the emoji change threshold. Number 100 determines how many clicks you need to type in order for the current emoji to change to the next one in the array. This creates a reward system and visual progress, motivating the user to click further.

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

Function encrypt_data — This is a custom encoder to protect the game's save data. it accepts the source data (data) as a string and a numeric key (key) by default 0x55 after that , the function processes each byte of the string: XOR (): Imposes a key on a byte using the "exclusive OR" operation, bit shift: "Shuffles" the bits of the encrypted byte, shifting them 2 positions to the left (<< 2) and combining with the result of shifting 6 positions to the right (>> 6Next, the function encodes the resulting set of bytes and converts them into a Base64-encoded text string so that it can be easily copied or entered.

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 is the inverse function for encrypt_data. It decrypts the data, the algorithm is briefly described below.:

  1. Decodes a string from Base64 back into bytes.
  2. Performs reverse bit shifts for each byte.: (byte >> 2) | (byte << 6).
  3. Applies an XOR operation with the same key (0x55) to return the original byte.
  4. Collects all bytes into a string and returns it.
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)

Function create_clicker_interface is the centerpiece of the entire application. Its main task is to dynamically generate and display a full—fledged game interface in the browser. To do this, it creates a large string containing all the necessary HTML, CSS, and JavaScript code. To ensure that multiple instances of the game do not conflict with each other, the function generates a unique identifier that is embedded in the names of all elements and functions. This ensures isolation and correct operation of the logic.

Embedded JavaScript code is responsible for all interactivity on the client side. Function handleClick processes each click on the emoji: increases the counter, plays a light compression animation, and checks if it's time to change the player's avatar to the next one based on the click threshold reached. This is the pure game mechanics.

The save system is implemented through a drop-down menu controlled by the function toggleDropdown. Function generateKey collects all the player's data (current score, emoji index and timestamp), forms a string from them and encodes it in Base64, creating the same "save key". Function loadKey performs the reverse operation: it decodes the key entered by the user, extracts data from it and restores the game state, allowing you to continue from the interrupted place.

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
😀

Conclusion

In conclusion, we can say that this project is a successful synthesis of high-performance computing on Julia and modern web technologies, implementing full-fledged game mechanics in an interactive web interface. The key feature is the architectural solution, in which the entire complex — from backend logic and cryptographic functions to the generation of a visually appealing frontend - is encapsulated in a single code base, which demonstrates its versatility not only in the scientific field, but also in the field of creating practical web applications with elements of game design and data protection.