AnyMath 文档
Notebook

Emoji Clicker

在编程领域,开发小型但功能完备的游戏是磨练技能和学习新技术的一种绝佳方式。Emoji Clicker 便是此类项目之一——这是一款网页游戏,玩家通过点击表情符号来增加计数器数值。 尽管看似简单,但这款游戏的幕后却隐藏着多种技术的强大融合:后端采用高性能的Julia语言,前端采用经典的HTML/CSS/JavaScript,并运用加密技术保障数据安全。

这款游戏绝不仅仅是一款点击类游戏。 它体现了现代开发理念:服务器端逻辑、图形界面和用户交互均由基于 Julia 的统一代码库创建和管理,从而消除了传统桌面编程与网页开发之间的界限。

游戏目标简单且引人入胜:只需点击屏幕中央的表情符号。 每次点击都会增加计数器数值。为了让游戏过程更具视觉趣味性,表情符号每点击100次就会更换一次,从笑脸 😀 到独角兽 🦄,经历完整的进化过程。 游戏还包含一套完善的进度保存系统,你的成就(点击次数、当前头像、时间)会被加密为一个特殊密钥,你可以复制该密钥,并在日后输入以从同一位置继续游戏。

接下来进入实现阶段,我们需要以下库:

  • using Base64 — 用于将数据编码和解码为 Base64 格式。用于将二进制数据(你的存档)转换为文本字符串,以便轻松复制或输入。

  • using Dates — 用于处理日期和时间。用于根据当前时间生成唯一标识符,并在存档中添加时间戳。

In [ ]:
using Base64, Dates

现在我们来声明项目的关键常量:

  • const EMOJIS — 这是一个包含游戏中将要使用的表情符号的数组。每个元素都是玩家的视觉化头像,会随着游戏进度而变化。

  • 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_dataencrypt_data 的逆向函数。它负责解密数据,其算法简要描述如下:

  1. 将 Base64 字符串解码回字节。
  2. 对每个字节执行反向位移操作: (byte >> 2) | (byte << 6).
  3. 使用相同的密钥(0x55 )进行异或(XOR)运算,以恢复原始字节。
  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 语言的高性能计算与现代 Web 技术,在交互式 Web 界面中实现了完整的游戏机制。 其关键特征在于架构设计:从后端逻辑和加密函数到生成视觉效果出众的前端界面,整个系统 ——均封装在统一的代码库中,这不仅彰显了其在科学领域的通用性,也体现了其在开发融合游戏设计元素和数据保护功能的实用Web应用程序方面的广泛适用性。