【教学类-150-03】20261108蝴蝶连连看html(一页2个版面、3个版面)

一、背景需求

前期已经做了单页面10关蝴蝶成对连连看

【教学类-150-02】20261027蝴蝶连连看html(图片缩小、消失速度快)+小3班屏幕测试https://mp.csdn.net/mp_blog/creation/editor/169266382

同时也破解了各个希沃屏幕显示效果不一样的问题(浏览器缩放比例。

教学类-07-16】20261108一分园、二分园希沃屏幕测试("蝴蝶连连看""破译电话号码"HTML)+破解不显示问题:缩放比例https://mp.csdn.net/mp_blog/creation/editor/170285964

存在问题:

一个屏幕只能1位孩子操作,会争抢操作玩,幼儿等待时间太长

二、代码修正(2个页面)

python 复制代码
'''
蝴蝶连连看 - 双人对战版(左右独立,各10关)
自动扫描文件夹内的 JPG 作为蝴蝶图案,左右两个玩家互不影响。
Deepseek,阿夏
20261108
'''

import os
import glob
import base64

def generate_image_based_game():
    # ================= 配置区 =================
    path = r'D:\000Python网页html\20261026蝴蝶连连看'
    img_folder = os.path.join(path, 'JPG')
    html_save_path = os.path.join(path, '009蝴蝶连连看_双人对战版.html')
    # =========================================

    # 1. 检查并读取图片
    if not os.path.exists(img_folder):
        os.makedirs(img_folder)
        print(f'❌ 找不到文件夹 {img_folder}')
        print(f'✅ 已自动为您创建该文件夹,请把 16 张图片(jpg/jpeg/png)放进去,然后重新运行代码!')
        return

    valid_ext = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
    image_files = [
        os.path.join(img_folder, f)
        for f in os.listdir(img_folder)
        if f.lower().endswith(valid_ext)
    ]

    if len(image_files) == 0:
        print(f'❌ 在 {img_folder} 中没有找到任何 .jpg/.jpeg/.png 文件,请检查!')
        return

    print(f'找到 {len(image_files)} 张图片,正在转换为离线数据...')

    mime_map = {
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.png': 'image/png',
        '.bmp': 'image/bmp',
        '.webp': 'image/webp',
    }

    base64_images = []
    for img_path in image_files:
        try:
            ext = os.path.splitext(img_path)[1].lower()
            mime = mime_map.get(ext, 'image/jpeg')
            with open(img_path, 'rb') as f:
                b64_str = base64.b64encode(f.read()).decode('utf-8')
                base64_images.append(f"data:{mime};base64,{b64_str}")
        except Exception as e:
            print(f"读取图片 {img_path} 失败: {e}")

    if not base64_images:
        print('❌ 图片读取失败,无法生成。')
        return

    js_images_array = '[\n' + ',\n'.join([f'"{img}"' for img in base64_images]) + '\n]'

    # 2. 生成双人版 HTML 内容
    html_content = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>双人蝴蝶连连看 · 10关挑战版</title>
    <style>
        * {{
            user-select: none;
            -webkit-tap-highlight-color: transparent;
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }}

        html, body {{
            width: 100%;
            height: 100%;
            overflow: hidden;
            position: fixed;
            top: 0;
            left: 0;
            background: linear-gradient(145deg, #1b2a47 0%, #0d1321 100%);
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', 'Comic Neue', 'Comic Sans MS', 'Chalkboard SE', cursive, sans-serif;
            touch-action: none;
            padding: 6px;
        }}

        .dual-container {{
            display: flex;
            width: 100%;
            max-width: 1200px;
            height: 100%;
            max-height: 98vh;
            gap: 12px;
            justify-content: center;
            align-items: stretch;
        }}

        .player-panel {{
            flex: 1 1 0;
            min-width: 0;
            background: #fdf8e7;
            border-radius: 40px;
            box-shadow: 0 15px 25px rgba(0,0,0,0.4), inset 0 1px 3px rgba(255,255,200,0.8);
            padding: 12px 12px 16px 12px;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }}

        .player-title {{
            text-align: center;
            font-weight: bold;
            font-size: 1.1rem;
            padding: 4px 0 6px 0;
            color: #3d2a1a;
            letter-spacing: 2px;
            border-radius: 40px;
            margin-bottom: 6px;
        }}
        .player-left .player-title {{
            background: #ffd9b3;
            box-shadow: inset 0 1px 4px #ffe0b0, 0 3px 0 #c49a6c;
        }}
        .player-right .player-title {{
            background: #b3e0ff;
            box-shadow: inset 0 1px 4px #cceeff, 0 3px 0 #6c9ac4;
        }}

        .info-panel {{
            display: flex;
            justify-content: space-between;
            align-items: baseline;
            background: #f5e5c8;
            padding: 6px 10px;
            border-radius: 60px;
            margin-bottom: 8px;
            box-shadow: inset 0 1px 4px rgba(0,0,0,0.05), 0 4px 8px rgba(0,0,0,0.1);
            flex-wrap: wrap;
            gap: 4px;
        }}

        .score-box, .level-box {{
            background: #5a3e2b;
            color: #ffefb9;
            padding: 3px 10px;
            border-radius: 40px;
            font-weight: bold;
            font-size: 1rem;
            text-shadow: 0 2px 0 #2e1e12;
            box-shadow: inset 0 1px 2px rgba(255,255,200,0.3), 0 3px 6px rgba(0,0,0,0.2);
            white-space: nowrap;
        }}

        .score-box span, .level-box span {{
            font-size: 1.2rem;
            margin-left: 4px;
            color: #ffd966;
        }}

        .grid-wrapper {{
            background: #fffaec;
            border-radius: 30px;
            padding: 8px;
            box-shadow: inset 0 0 0 2px #fff9e6, inset 0 0 0 4px #e7dbb8, 0 8px 14px rgba(0,0,0,0.15);
            flex: 1 1 auto;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 0;
        }}

        .game-grid {{
            display: grid;
            grid-template-columns: repeat(6, 1fr);
            gap: 6px;
            justify-items: center;
            align-items: center;
            width: 100%;
            height: 100%;
            aspect-ratio: 1 / 1;
            max-width: 100%;
            max-height: 100%;
        }}

        .card {{
            aspect-ratio: 1 / 1;
            width: 100%;
            border-radius: 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: all 0.12s linear;
            box-shadow: 0 4px 0 rgba(0,0,0,0.2);
            transform: translateY(-1px);
            background-color: #fff;
            border: 2px solid #fff;
            min-width: 0;
            min-height: 0;
        }}

        .card img {{
            width: 90%;
            height: 90%;
            object-fit: contain;
            filter: drop-shadow(0 1px 1px rgba(0,0,0,0.15));
            transition: transform 0.3s;
            pointer-events: none;
        }}

        .card:hover img {{
            transform: scale(1.1);
        }}

        .card.selected {{
            box-shadow: 0 0 0 3px #ffb347, 0 0 0 6px #ffe484, 0 4px 0 rgba(0,0,0,0.2);
            transform: scale(0.95) translateY(0px);
        }}

        .card.removed {{
            visibility: hidden;
            pointer-events: none;
            box-shadow: none;
            transform: scale(0.8);
            background: transparent;
        }}

        .action-buttons {{
            margin-top: 8px;
            display: flex;
            justify-content: center;
            gap: 12px;
            flex-shrink: 0;
        }}

        button {{
            background: #ffb347;
            border: none;
            font-family: inherit;
            font-weight: bold;
            font-size: 0.8rem;
            padding: 0;
            width: 80px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 18px;
            color: #3d2a1a;
            cursor: pointer;
            box-shadow: 0 3px 0 #a45d2e;
            transition: 0.07s linear;
            flex-shrink: 0;
        }}

        button:active {{
            transform: translateY(2px);
            box-shadow: 0 1px 0 #a45d2e;
        }}

        .message-area {{
            margin-top: 8px;
            background: #e9dbbc;
            border-radius: 30px;
            padding: 4px 8px;
            font-size: 0.8rem;
            font-weight: bold;
            color: #674e2c;
            min-height: 2.4rem;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            line-height: 1.3;
            flex-shrink: 0;
            word-break: break-word;
        }}

        @keyframes celebrate {{
            0% {{ transform: scale(1); }}
            50% {{ transform: scale(1.1); background: radial-gradient(circle, gold, #ffaa00); }}
            100% {{ transform: scale(1); }}
        }}

        .celebrate-card {{
            animation: celebrate 0.5s ease-in-out;
        }}

        @media (max-width: 700px) {{
            .player-panel {{
                padding: 8px 6px 12px 6px;
                border-radius: 28px;
            }}
            .info-panel {{
                padding: 4px 6px;
            }}
            .score-box, .level-box {{
                font-size: 0.8rem;
                padding: 2px 6px;
            }}
            .score-box span, .level-box span {{
                font-size: 1rem;
            }}
            .card {{
                border-radius: 10px;
            }}
            button {{
                width: 64px;
                height: 28px;
                font-size: 0.7rem;
            }}
            .message-area {{
                font-size: 0.7rem;
                min-height: 2rem;
            }}
            .game-grid {{
                gap: 4px;
            }}
        }}
    </style>
</head>
<body>
<div class="dual-container">
    <!-- 左侧玩家 -->
    <div class="player-panel player-left" id="playerLeft">
        <div class="player-title">🦋 左边 · 小蝴蝶</div>
        <div class="info-panel">
            <div class="score-box">🦋 积分 <span id="scoreValueLeft">0</span></div>
            <div class="level-box">✨ 第 <span id="levelValueLeft">1</span> / 10 关</div>
        </div>
        <div class="grid-wrapper">
            <div class="game-grid" id="gameGridLeft"></div>
        </div>
        <div class="action-buttons">
            <button id="shuffleBtnLeft">🔄 洗牌</button>
            <button id="refreshHintBtnLeft">✨ 提示</button>
        </div>
        <div class="message-area" id="messageAreaLeft">
            🦋 难度递增挑战开始啦!找找看哪两只是一样的吧~
        </div>
    </div>

    <!-- 右侧玩家 -->
    <div class="player-panel player-right" id="playerRight">
        <div class="player-title">🦋 右边 · 小蝴蝶</div>
        <div class="info-panel">
            <div class="score-box">🦋 积分 <span id="scoreValueRight">0</span></div>
            <div class="level-box">✨ 第 <span id="levelValueRight">1</span> / 10 关</div>
        </div>
        <div class="grid-wrapper">
            <div class="game-grid" id="gameGridRight"></div>
        </div>
        <div class="action-buttons">
            <button id="shuffleBtnRight">🔄 洗牌</button>
            <button id="refreshHintBtnRight">✨ 提示</button>
        </div>
        <div class="message-area" id="messageAreaRight">
            🦋 难度递增挑战开始啦!找找看哪两只是一样的吧~
        </div>
    </div>
</div>

<script>
    // ==================== 内嵌的高清蝴蝶图片数据 ====================
    const BUTTERFLY_IMAGES = {js_images_array};

    // ==================== 常量 ====================
    const ROWS = 6;
    const COLS = 6;
    const TOTAL_CELLS = ROWS * COLS;
    const MAX_LEVEL = 10;

    // ==================== 游戏类封装(每个玩家一个实例) ====================
    class ButterflyGame {{
        constructor(playerId, dom) {{
            this.playerId = playerId;
            this.dom = dom;

            this.gridContainer = document.getElementById(dom.grid);
            this.scoreSpan = document.getElementById(dom.score);
            this.levelSpan = document.getElementById(dom.level);
            this.messageDiv = document.getElementById(dom.message);
            this.shuffleBtn = document.getElementById(dom.shuffleBtn);
            this.hintBtn = document.getElementById(dom.hintBtn);

            this.currentLevel = 1;
            this.currentScore = 0;
            this.gridData = [];
            this.selectedRow = null;
            this.selectedCol = null;
            this.audioContext = null;
            this.hintTimer = null;

            this.shuffleBtn.addEventListener('click', () => this.shuffleBoard());
            this.hintBtn.addEventListener('click', () => this.giveHint());

            this.resetGame();
        }}

        // ---------- 音频 ----------
        playVictoryMusic() {{
            try {{
                if (!this.audioContext) {{
                    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    if (this.audioContext.state === 'suspended') this.audioContext.resume();
                }}
                const now = this.audioContext.currentTime;
                const notes = [523.25, 587.33, 659.25, 698.46, 783.99, 880.00, 987.77, 1046.50];
                notes.forEach((freq, index) => {{
                    const oscillator = this.audioContext.createOscillator();
                    const gainNode = this.audioContext.createGain();
                    oscillator.connect(gainNode);
                    gainNode.connect(this.audioContext.destination);
                    oscillator.frequency.value = freq;
                    oscillator.type = 'sine';
                    gainNode.gain.setValueAtTime(0.3, now + index * 0.15);
                    gainNode.gain.exponentialRampToValueAtTime(0.0001, now + (index * 0.15) + 0.3);
                    oscillator.start(now + index * 0.15);
                    oscillator.stop(now + (index * 0.15) + 0.3);
                }});
            }} catch (e) {{}}
        }}

        playCheerSound() {{
            try {{
                if (!this.audioContext) {{
                    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    if (this.audioContext.state === 'suspended') this.audioContext.resume();
                }}
                const now = this.audioContext.currentTime;
                const oscillator = this.audioContext.createOscillator();
                const gainNode = this.audioContext.createGain();
                oscillator.connect(gainNode);
                gainNode.connect(this.audioContext.destination);
                oscillator.frequency.value = 800;
                oscillator.type = 'sine';
                gainNode.gain.setValueAtTime(0.2, now);
                gainNode.gain.exponentialRampToValueAtTime(0.0001, now + 0.2);
                oscillator.start(now);
                oscillator.stop(now + 0.2);
            }} catch (e) {{}}
        }}

        speak(text) {{
            if ('speechSynthesis' in window) {{
                window.speechSynthesis.cancel();
                const utterance = new SpeechSynthesisUtterance(text);
                utterance.lang = 'zh-CN';
                utterance.rate = 0.9;
                utterance.pitch = 1.2;
                const voices = window.speechSynthesis.getVoices();
                const chineseVoice = voices.find(voice => voice.lang === 'zh-CN');
                if (chineseVoice) utterance.voice = chineseVoice;
                window.speechSynthesis.speak(utterance);
            }}
        }}

        // ---------- UI ----------
        updateScoreUI() {{ this.scoreSpan.innerText = this.currentScore; }}
        updateLevelUI() {{ this.levelSpan.innerText = this.currentLevel; }}

        showMessage(msg, isError = false) {{
            this.messageDiv.innerHTML = `${{isError ? '⚠️ ' : '🍬 '}} ${{msg}}`;
        }}

        clearSelected() {{
            this.selectedRow = null;
            this.selectedCol = null;
            this.renderGrid();
        }}

        // ---------- 连通判断 ----------
        canConnect(row1, col1, row2, col2, data) {{
            if (row1 === row2 && col1 === col2) return false;
            if (data[row1][col1] === null || data[row2][col2] === null) return false;
            if (data[row1][col1] !== data[row2][col2]) return false;

            const rows = data.length;
            const cols = data[0].length;

            const checkRowClear = (r, cA, cB) => {{
                let minC = Math.min(cA, cB), maxC = Math.max(cA, cB);
                for (let i = minC + 1; i < maxC; i++) if (data[r][i] !== null) return false;
                return true;
            }};
            const checkColClear = (c, rA, rB) => {{
                let minR = Math.min(rA, rB), maxR = Math.max(rA, rB);
                for (let i = minR + 1; i < maxR; i++) if (data[i][c] !== null) return false;
                return true;
            }};

            if (row1 === row2 && checkRowClear(row1, col1, col2)) return true;
            if (col1 === col2 && checkColClear(col1, row1, row2)) return true;
            if (data[row1][col2] === null && checkRowClear(row1, col1, col2) && checkColClear(col2, row1, row2)) return true;
            if (data[row2][col1] === null && checkColClear(col1, row1, row2) && checkRowClear(row2, col1, col2)) return true;

            for (let i = 0; i < rows; i++) {{
                if (i !== row1 && i !== row2 && data[i][col1] === null && data[i][col2] === null &&
                    checkColClear(col1, row1, i) && checkRowClear(i, col1, col2) && checkColClear(col2, i, row2)) return true;
            }}
            for (let j = 0; j < cols; j++) {{
                if (j !== col1 && j !== col2 && data[row1][j] === null && data[row2][j] === null &&
                    checkRowClear(row1, col1, j) && checkColClear(j, row1, row2) && checkRowClear(row2, j, col2)) return true;
            }}
            return false;
        }}

        isWin() {{
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) return false;
            return true;
        }}

        hasRemainingMoves() {{
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    if (this.gridData[i][j] !== null) {{
                        for (let m = 0; m < ROWS; m++) {{
                            for (let n = 0; n < COLS; n++) {{
                                if (i === m && j === n) continue;
                                if (this.gridData[m][n] !== null && this.gridData[i][j] === this.gridData[m][n] &&
                                    this.canConnect(i, j, m, n, this.gridData)) return true;
                            }}
                        }}
                    }}
                }}
            }}
            return false;
        }}

        // ---------- 操作 ----------
        shuffleBoard() {{
            let typeList = [];
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) typeList.push(this.gridData[i][j]);
            for (let i = typeList.length - 1; i > 0; i--) {{
                const j = Math.floor(Math.random() * (i + 1));
                [typeList[i], typeList[j]] = [typeList[j], typeList[i]];
            }}
            let idx = 0;
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) this.gridData[i][j] = typeList[idx++];
            this.renderGrid();
            this.clearSelected();
            this.showMessage("♻️ 重新洗牌啦,继续挑战漂亮的蝴蝶吧", false);
            this.playCheerSound();
        }}

        giveHint() {{
            if (this.hintTimer) clearTimeout(this.hintTimer);
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    if (this.gridData[i][j] !== null) {{
                        for (let m = 0; m < ROWS; m++) {{
                            for (let n = 0; n < COLS; n++) {{
                                if (i === m && j === n) continue;
                                if (this.gridData[m][n] !== null && this.gridData[i][j] === this.gridData[m][n] &&
                                    this.canConnect(i, j, m, n, this.gridData)) {{
                                    this.highlightPair(i, j, m, n);
                                    this.showMessage(`💡 试试消除这只蝴蝶吧!`, false);
                                    this.playCheerSound();
                                    return;
                                }}
                            }}
                        }}
                    }}
                }}
            }}
            this.showMessage("🤔 暂时没有可消除的蝴蝶,试试洗牌吧!", true);
        }}

        highlightPair(row1, col1, row2, col2) {{
            const cards = this.gridContainer.querySelectorAll('.card');
            const idx1 = row1 * COLS + col1;
            const idx2 = row2 * COLS + col2;
            if (cards[idx1] && cards[idx2]) {{
                cards[idx1].style.boxShadow = "0 0 0 3px gold, 0 0 0 6px #ffb347, 0 4px 0 rgba(0,0,0,0.2)";
                cards[idx2].style.boxShadow = "0 0 0 3px gold, 0 0 0 6px #ffb347, 0 4px 0 rgba(0,0,0,0.2)";
                this.hintTimer = setTimeout(() => this.renderGrid(), 1800);
            }}
        }}

        celebrateLevelCompletion() {{
            const message = `恭喜通关!当前总得分 ${{this.currentScore}} 分!`;
            this.playVictoryMusic();
            setTimeout(() => this.speak(message), 300);
            this.showMessage(`🎉🎉🎉 ${{message}} 🎉🎉🎉`, false);
            const cards = this.gridContainer.querySelectorAll('.card:not(.removed)');
            cards.forEach((card, index) => {{
                setTimeout(() => {{
                    card.classList.add('celebrate-card');
                    setTimeout(() => card.classList.remove('celebrate-card'), 500);
                }}, index * 30);
            }});
        }}

        eliminatePair(row1, col1, row2, col2) {{
            if (!this.canConnect(row1, col1, row2, col2, this.gridData)) return false;
            this.gridData[row1][col1] = null;
            this.gridData[row2][col2] = null;
            this.currentScore += 10;
            this.updateScoreUI();
            this.renderGrid();
            this.clearSelected();
            this.playCheerSound();
            this.showMessage(`🎉 +10分!消除成功!`, false);

            if (this.isWin()) {{
                this.celebrateLevelCompletion();
                setTimeout(() => {{
                    if (this.currentLevel < MAX_LEVEL) {{
                        this.currentLevel++;
                        this.updateLevelUI();
                        this.initLevelGrid();
                        this.clearSelected();
                        this.showMessage(`🎉 进入第 ${{this.currentLevel}} 关!难度提升啦~ 🎉`, false);
                        setTimeout(() => {{ if (!this.hasRemainingMoves()) this.shuffleBoard(); }}, 200);
                    }} else {{
                        this.playVictoryMusic();
                        this.speak("太厉害了!你已完成了全部10关挑战!");
                        this.showMessage(`🏆 恭喜你!已完成全部10关挑战!最终得分 ${{this.currentScore}} 分!`, false);
                    }}
                }}, 2000);
                return true;
            }}
            if (!this.hasRemainingMoves()) setTimeout(() => {{ if (!this.hasRemainingMoves()) this.shuffleBoard(); }}, 200);
            return true;
        }}

        // ---------- 渲染 ----------
        renderGrid() {{
            this.gridContainer.innerHTML = '';
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    const type = this.gridData[i][j];
                    const card = document.createElement('div');
                    card.className = 'card';
                    if (type === null) {{
                        card.classList.add('removed');
                    }} else {{
                        card.innerHTML = `<img src="${{BUTTERFLY_IMAGES[type]}}" alt="蝴蝶">`;
                    }}
                    if (this.selectedRow === i && this.selectedCol === j && type !== null) {{
                        card.classList.add('selected');
                    }}
                    card.addEventListener('click', (function(game, r, c) {{
                        return function() {{ game.onCardClick(r, c); }};
                    }})(this, i, j));
                    this.gridContainer.appendChild(card);
                }}
            }}
        }}

        onCardClick(row, col) {{
            if (this.gridData[row][col] === null) return;
            this.playCheerSound();

            if (this.selectedRow === null || this.selectedCol === null) {{
                this.selectedRow = row;
                this.selectedCol = col;
                this.renderGrid();
                this.showMessage(`选中了蝴蝶,再点击另一只相同蝴蝶消除~`, false);
                return;
            }}

            const sRow = this.selectedRow, sCol = this.selectedCol;
            if (sRow === row && sCol === col) {{
                this.clearSelected();
                this.showMessage("已取消选中", false);
                return;
            }}

            if (this.gridData[sRow][sCol] !== null && this.gridData[row][col] !== null &&
                this.gridData[sRow][sCol] === this.gridData[row][col] &&
                this.canConnect(sRow, sCol, row, col, this.gridData)) {{
                this.eliminatePair(sRow, sCol, row, col);
            }} else {{
                if (this.gridData[row][col] !== null) {{
                    if (this.gridData[sRow][sCol] !== this.gridData[row][col]) {{
                        this.showMessage(`🚫 两只蝴蝶不一样,不能消除哦`, true);
                    }} else {{
                        this.showMessage(`🚫 路径不通,最多拐两弯~ 再试试洗牌吧`, true);
                    }}
                    this.selectedRow = row;
                    this.selectedCol = col;
                    this.renderGrid();
                }} else {{
                    this.clearSelected();
                }}
            }}
        }}

        // ---------- 初始化关卡 ----------
        initLevelGrid() {{
            let typesCount = Math.floor(4 + (this.currentLevel - 1) * (16 - 4) / (MAX_LEVEL - 1));
            typesCount = Math.min(typesCount, BUTTERFLY_IMAGES.length);

            let availableIndices = Array.from({{ length: BUTTERFLY_IMAGES.length }}, (_, i) => i);
            availableIndices.sort(() => Math.random() - 0.5);
            let selectedTypes = availableIndices.slice(0, typesCount);

            let pool = [];
            let baseCount = Math.floor(TOTAL_CELLS / typesCount);
            if (baseCount % 2 !== 0) baseCount -= 1;

            for (let i = 0; i < typesCount; i++) {{
                for (let k = 0; k < baseCount; k++) pool.push(selectedTypes[i]);
            }}

            let remainingCells = TOTAL_CELLS - pool.length;
            while (remainingCells > 0) {{
                let randomType = selectedTypes[Math.floor(Math.random() * typesCount)];
                pool.push(randomType);
                pool.push(randomType);
                remainingCells -= 2;
            }}

            for (let i = pool.length - 1; i > 0; i--) {{
                const j = Math.floor(Math.random() * (i + 1));
                [pool[i], pool[j]] = [pool[j], pool[i]];
            }}

            this.gridData = Array(ROWS).fill().map(() => Array(COLS).fill(null));
            let idx = 0;
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) this.gridData[i][j] = pool[idx++];

            this.showMessage(`当前关卡使用了 ${{typesCount}} 种蝴蝶图案,加油!`, false);
            if (!this.hasRemainingMoves()) this.shuffleBoard();
            this.renderGrid();
        }}

        resetGame() {{
            this.currentLevel = 1;
            this.currentScore = 0;
            this.updateScoreUI();
            this.updateLevelUI();
            this.initLevelGrid();
            this.clearSelected();
            this.showMessage("🦋 10关挑战开始啦!找找看哪两只是一样的吧~", false);
            if ('speechSynthesis' in window) window.speechSynthesis.getVoices();
        }}
    }}

    // ==================== 启动双人游戏 ====================
    window.addEventListener('load', () => {{
        if (!BUTTERFLY_IMAGES || BUTTERFLY_IMAGES.length < 4) {{
            alert("图片数量太少,请确保 jpg 文件夹中有至少 4 张图片!");
            return;
        }}

        // 左侧游戏实例
        const gameLeft = new ButterflyGame('left', {{
            grid: 'gameGridLeft',
            score: 'scoreValueLeft',
            level: 'levelValueLeft',
            message: 'messageAreaLeft',
            shuffleBtn: 'shuffleBtnLeft',
            hintBtn: 'refreshHintBtnLeft'
        }});

        // 右侧游戏实例
        const gameRight = new ButterflyGame('right', {{
            grid: 'gameGridRight',
            score: 'scoreValueRight',
            level: 'levelValueRight',
            message: 'messageAreaRight',
            shuffleBtn: 'shuffleBtnRight',
            hintBtn: 'refreshHintBtnRight'
        }});

        // 防滚动
        document.body.addEventListener('touchmove', e => e.preventDefault(), {{ passive: false }});
        document.body.addEventListener('wheel', e => e.preventDefault(), {{ passive: false }});

        if ('speechSynthesis' in window) window.speechSynthesis.getVoices();
    }});
</script>
</body>
</html>'''

    # 3. 写入文件
    try:
        os.makedirs(os.path.dirname(html_save_path), exist_ok=True)
        with open(html_save_path, 'w', encoding='utf-8') as f:
            f.write(html_content)
        print(f'✅ 双人对战版连连看生成成功!左右独立,各10关。')
        print(f'📁 文件保存位置: {html_save_path}')
    except Exception as e:
        print(f'❌ 生成文件失败: {e}')

if __name__ == '__main__':
    generate_image_based_game()

我再笔记本电脑上测试一下,似乎没有什么问题,所以我想做3个页面。

三、代码修正(3个页面)

3个页面的话列数就要从6列改成5列。

python 复制代码
'''
蝴蝶连连看 - 三人对战版(左中右独立,各10关,5列x6行)
自动扫描文件夹内的 JPG 作为蝴蝶图案,三个玩家互不影响。
Deepseek,阿夏
20261108
'''

import os
import glob
import base64

def generate_image_based_game():
    # ================= 配置区 =================
    path = r'D:\000Python网页html\20261026蝴蝶连连看'
    img_folder = os.path.join(path, 'JPG')
    html_save_path = os.path.join(path, '010蝴蝶连连看_三人对战版.html')
    # =========================================

    # 1. 检查并读取图片
    if not os.path.exists(img_folder):
        os.makedirs(img_folder)
        print(f'❌ 找不到文件夹 {img_folder}')
        print(f'✅ 已自动为您创建该文件夹,请把 16 张图片(jpg/jpeg/png)放进去,然后重新运行代码!')
        return

    valid_ext = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
    image_files = [
        os.path.join(img_folder, f)
        for f in os.listdir(img_folder)
        if f.lower().endswith(valid_ext)
    ]

    if len(image_files) == 0:
        print(f'❌ 在 {img_folder} 中没有找到任何 .jpg/.jpeg/.png 文件,请检查!')
        return

    print(f'找到 {len(image_files)} 张图片,正在转换为离线数据...')

    mime_map = {
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.png': 'image/png',
        '.bmp': 'image/bmp',
        '.webp': 'image/webp',
    }

    base64_images = []
    for img_path in image_files:
        try:
            ext = os.path.splitext(img_path)[1].lower()
            mime = mime_map.get(ext, 'image/jpeg')
            with open(img_path, 'rb') as f:
                b64_str = base64.b64encode(f.read()).decode('utf-8')
                base64_images.append(f"data:{mime};base64,{b64_str}")
        except Exception as e:
            print(f"读取图片 {img_path} 失败: {e}")

    if not base64_images:
        print('❌ 图片读取失败,无法生成。')
        return

    js_images_array = '[\n' + ',\n'.join([f'"{img}"' for img in base64_images]) + '\n]'

    # 2. 生成三人版 HTML 内容
    html_content = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>三人蝴蝶连连看 · 10关挑战版</title>
    <style>
        * {{
            user-select: none;
            -webkit-tap-highlight-color: transparent;
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }}

        html, body {{
            width: 100%;
            height: 100%;
            overflow: hidden;
            position: fixed;
            top: 0;
            left: 0;
            background: linear-gradient(145deg, #1b2a47 0%, #0d1321 100%);
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', 'Comic Neue', 'Comic Sans MS', 'Chalkboard SE', cursive, sans-serif;
            touch-action: none;
            padding: 4px;
        }}

        .triple-container {{
            display: flex;
            width: 100%;
            max-width: 1500px;
            height: 100%;
            max-height: 98vh;
            gap: 8px;
            justify-content: center;
            align-items: stretch;
        }}

        .player-panel {{
            flex: 1 1 0;
            min-width: 0;
            background: #fdf8e7;
            border-radius: 32px;
            box-shadow: 0 12px 20px rgba(0,0,0,0.4), inset 0 1px 3px rgba(255,255,200,0.8);
            padding: 8px 8px 12px 8px;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }}

        .player-title {{
            text-align: center;
            font-weight: bold;
            font-size: 0.95rem;
            padding: 3px 0 5px 0;
            color: #3d2a1a;
            letter-spacing: 1px;
            border-radius: 40px;
            margin-bottom: 4px;
            flex-shrink: 0;
        }}
        .player-left .player-title {{
            background: #ffd9b3;
            box-shadow: inset 0 1px 4px #ffe0b0, 0 3px 0 #c49a6c;
        }}
        .player-middle .player-title {{
            background: #c8e6c9;
            box-shadow: inset 0 1px 4px #d4f0d6, 0 3px 0 #7a9e7e;
        }}
        .player-right .player-title {{
            background: #b3e0ff;
            box-shadow: inset 0 1px 4px #cceeff, 0 3px 0 #6c9ac4;
        }}

        .info-panel {{
            display: flex;
            justify-content: space-between;
            align-items: baseline;
            background: #f5e5c8;
            padding: 4px 6px;
            border-radius: 60px;
            margin-bottom: 5px;
            box-shadow: inset 0 1px 4px rgba(0,0,0,0.05), 0 3px 6px rgba(0,0,0,0.1);
            flex-wrap: wrap;
            gap: 2px;
            flex-shrink: 0;
        }}

        .score-box, .level-box {{
            background: #5a3e2b;
            color: #ffefb9;
            padding: 2px 6px;
            border-radius: 40px;
            font-weight: bold;
            font-size: 0.7rem;
            text-shadow: 0 2px 0 #2e1e12;
            box-shadow: inset 0 1px 2px rgba(255,255,200,0.3), 0 2px 4px rgba(0,0,0,0.2);
            white-space: nowrap;
        }}

        .score-box span, .level-box span {{
            font-size: 0.85rem;
            margin-left: 2px;
            color: #ffd966;
        }}

        .grid-wrapper {{
            background: #fffaec;
            border-radius: 24px;
            padding: 5px;
            box-shadow: inset 0 0 0 2px #fff9e6, inset 0 0 0 3px #e7dbb8, 0 6px 10px rgba(0,0,0,0.15);
            flex: 1 1 auto;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 0;
        }}

        .game-grid {{
            display: grid;
            grid-template-columns: repeat(5, 1fr);
            gap: 4px;
            justify-items: center;
            align-items: center;
            width: 100%;
            height: 100%;
            aspect-ratio: 5 / 6;
            max-width: 100%;
            max-height: 100%;
        }}

        .card {{
            aspect-ratio: 1 / 1;
            width: 100%;
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: all 0.12s linear;
            box-shadow: 0 3px 0 rgba(0,0,0,0.2);
            transform: translateY(-1px);
            background-color: #fff;
            border: 1.5px solid #fff;
            min-width: 0;
            min-height: 0;
        }}

        .card img {{
            width: 90%;
            height: 90%;
            object-fit: contain;
            filter: drop-shadow(0 1px 1px rgba(0,0,0,0.15));
            transition: transform 0.3s;
            pointer-events: none;
        }}

        .card:hover img {{
            transform: scale(1.1);
        }}

        .card.selected {{
            box-shadow: 0 0 0 2px #ffb347, 0 0 0 5px #ffe484, 0 3px 0 rgba(0,0,0,0.2);
            transform: scale(0.95) translateY(0px);
        }}

        .card.removed {{
            visibility: hidden;
            pointer-events: none;
            box-shadow: none;
            transform: scale(0.8);
            background: transparent;
        }}

        .action-buttons {{
            margin-top: 5px;
            display: flex;
            justify-content: center;
            gap: 8px;
            flex-shrink: 0;
        }}

        button {{
            background: #ffb347;
            border: none;
            font-family: inherit;
            font-weight: bold;
            font-size: 0.65rem;
            padding: 0;
            width: 60px;
            height: 26px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 14px;
            color: #3d2a1a;
            cursor: pointer;
            box-shadow: 0 2px 0 #a45d2e;
            transition: 0.07s linear;
            flex-shrink: 0;
        }}

        button:active {{
            transform: translateY(2px);
            box-shadow: 0 0px 0 #a45d2e;
        }}

        .message-area {{
            margin-top: 5px;
            background: #e9dbbc;
            border-radius: 24px;
            padding: 3px 6px;
            font-size: 0.65rem;
            font-weight: bold;
            color: #674e2c;
            min-height: 1.8rem;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            line-height: 1.2;
            flex-shrink: 0;
            word-break: break-word;
        }}

        @keyframes celebrate {{
            0% {{ transform: scale(1); }}
            50% {{ transform: scale(1.1); background: radial-gradient(circle, gold, #ffaa00); }}
            100% {{ transform: scale(1); }}
        }}

        .celebrate-card {{
            animation: celebrate 0.5s ease-in-out;
        }}

        /* 窄屏适配:三个面板并排时进一步缩小 */
        @media (max-width: 900px) {{
            .triple-container {{
                gap: 5px;
            }}
            .player-panel {{
                padding: 5px 4px 8px 4px;
                border-radius: 22px;
            }}
            .player-title {{
                font-size: 0.75rem;
                padding: 2px 0 3px 0;
            }}
            .score-box, .level-box {{
                font-size: 0.55rem;
                padding: 1px 4px;
            }}
            .score-box span, .level-box span {{
                font-size: 0.7rem;
            }}
            .game-grid {{
                gap: 3px;
            }}
            button {{
                width: 48px;
                height: 22px;
                font-size: 0.55rem;
                border-radius: 10px;
            }}
            .message-area {{
                font-size: 0.55rem;
                min-height: 1.5rem;
            }}
            .card {{
                border-radius: 7px;
            }}
        }}
    </style>
</head>
<body>
<div class="triple-container">
    <!-- 左侧玩家 -->
    <div class="player-panel player-left" id="playerLeft">
        <div class="player-title">🦋 左边</div>
        <div class="info-panel">
            <div class="score-box">🦋 <span id="scoreValueLeft">0</span></div>
            <div class="level-box">✨ <span id="levelValueLeft">1</span>/10</div>
        </div>
        <div class="grid-wrapper">
            <div class="game-grid" id="gameGridLeft"></div>
        </div>
        <div class="action-buttons">
            <button id="shuffleBtnLeft">🔄</button>
            <button id="refreshHintBtnLeft">✨</button>
        </div>
        <div class="message-area" id="messageAreaLeft">
            🦋 开始啦~
        </div>
    </div>

    <!-- 中间玩家 -->
    <div class="player-panel player-middle" id="playerMiddle">
        <div class="player-title">🦋 中间</div>
        <div class="info-panel">
            <div class="score-box">🦋 <span id="scoreValueMiddle">0</span></div>
            <div class="level-box">✨ <span id="levelValueMiddle">1</span>/10</div>
        </div>
        <div class="grid-wrapper">
            <div class="game-grid" id="gameGridMiddle"></div>
        </div>
        <div class="action-buttons">
            <button id="shuffleBtnMiddle">🔄</button>
            <button id="refreshHintBtnMiddle">✨</button>
        </div>
        <div class="message-area" id="messageAreaMiddle">
            🦋 开始啦~
        </div>
    </div>

    <!-- 右侧玩家 -->
    <div class="player-panel player-right" id="playerRight">
        <div class="player-title">🦋 右边</div>
        <div class="info-panel">
            <div class="score-box">🦋 <span id="scoreValueRight">0</span></div>
            <div class="level-box">✨ <span id="levelValueRight">1</span>/10</div>
        </div>
        <div class="grid-wrapper">
            <div class="game-grid" id="gameGridRight"></div>
        </div>
        <div class="action-buttons">
            <button id="shuffleBtnRight">🔄</button>
            <button id="refreshHintBtnRight">✨</button>
        </div>
        <div class="message-area" id="messageAreaRight">
            🦋 开始啦~
        </div>
    </div>
</div>

<script>
    // ==================== 内嵌的高清蝴蝶图片数据 ====================
    const BUTTERFLY_IMAGES = {js_images_array};

    // ==================== 常量(5列x6行) ====================
    const COLS = 5;
    const ROWS = 6;
    const TOTAL_CELLS = ROWS * COLS;  // 30
    const MAX_LEVEL = 10;

    // ==================== 游戏类封装 ====================
    class ButterflyGame {{
        constructor(playerId, dom) {{
            this.playerId = playerId;
            this.dom = dom;

            this.gridContainer = document.getElementById(dom.grid);
            this.scoreSpan = document.getElementById(dom.score);
            this.levelSpan = document.getElementById(dom.level);
            this.messageDiv = document.getElementById(dom.message);
            this.shuffleBtn = document.getElementById(dom.shuffleBtn);
            this.hintBtn = document.getElementById(dom.hintBtn);

            this.currentLevel = 1;
            this.currentScore = 0;
            this.gridData = [];
            this.selectedRow = null;
            this.selectedCol = null;
            this.audioContext = null;
            this.hintTimer = null;

            this.shuffleBtn.addEventListener('click', () => this.shuffleBoard());
            this.hintBtn.addEventListener('click', () => this.giveHint());

            this.resetGame();
        }}

        // ---------- 音频 ----------
        playVictoryMusic() {{
            try {{
                if (!this.audioContext) {{
                    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    if (this.audioContext.state === 'suspended') this.audioContext.resume();
                }}
                const now = this.audioContext.currentTime;
                const notes = [523.25, 587.33, 659.25, 698.46, 783.99, 880.00, 987.77, 1046.50];
                notes.forEach((freq, index) => {{
                    const oscillator = this.audioContext.createOscillator();
                    const gainNode = this.audioContext.createGain();
                    oscillator.connect(gainNode);
                    gainNode.connect(this.audioContext.destination);
                    oscillator.frequency.value = freq;
                    oscillator.type = 'sine';
                    gainNode.gain.setValueAtTime(0.3, now + index * 0.15);
                    gainNode.gain.exponentialRampToValueAtTime(0.0001, now + (index * 0.15) + 0.3);
                    oscillator.start(now + index * 0.15);
                    oscillator.stop(now + (index * 0.15) + 0.3);
                }});
            }} catch (e) {{}}
        }}

        playCheerSound() {{
            try {{
                if (!this.audioContext) {{
                    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    if (this.audioContext.state === 'suspended') this.audioContext.resume();
                }}
                const now = this.audioContext.currentTime;
                const oscillator = this.audioContext.createOscillator();
                const gainNode = this.audioContext.createGain();
                oscillator.connect(gainNode);
                gainNode.connect(this.audioContext.destination);
                oscillator.frequency.value = 800;
                oscillator.type = 'sine';
                gainNode.gain.setValueAtTime(0.2, now);
                gainNode.gain.exponentialRampToValueAtTime(0.0001, now + 0.2);
                oscillator.start(now);
                oscillator.stop(now + 0.2);
            }} catch (e) {{}}
        }}

        speak(text) {{
            if ('speechSynthesis' in window) {{
                window.speechSynthesis.cancel();
                const utterance = new SpeechSynthesisUtterance(text);
                utterance.lang = 'zh-CN';
                utterance.rate = 0.9;
                utterance.pitch = 1.2;
                const voices = window.speechSynthesis.getVoices();
                const chineseVoice = voices.find(voice => voice.lang === 'zh-CN');
                if (chineseVoice) utterance.voice = chineseVoice;
                window.speechSynthesis.speak(utterance);
            }}
        }}

        // ---------- UI ----------
        updateScoreUI() {{ this.scoreSpan.innerText = this.currentScore; }}
        updateLevelUI() {{ this.levelSpan.innerText = this.currentLevel; }}

        showMessage(msg, isError = false) {{
            this.messageDiv.innerHTML = `${{isError ? '⚠️ ' : '🍬 '}} ${{msg}}`;
        }}

        clearSelected() {{
            this.selectedRow = null;
            this.selectedCol = null;
            this.renderGrid();
        }}

        // ---------- 连通判断(支持5列x6行) ----------
        canConnect(row1, col1, row2, col2, data) {{
            if (row1 === row2 && col1 === col2) return false;
            if (data[row1][col1] === null || data[row2][col2] === null) return false;
            if (data[row1][col1] !== data[row2][col2]) return false;

            const rows = data.length;
            const cols = data[0].length;

            const checkRowClear = (r, cA, cB) => {{
                let minC = Math.min(cA, cB), maxC = Math.max(cA, cB);
                for (let i = minC + 1; i < maxC; i++) if (data[r][i] !== null) return false;
                return true;
            }};
            const checkColClear = (c, rA, rB) => {{
                let minR = Math.min(rA, rB), maxR = Math.max(rA, rB);
                for (let i = minR + 1; i < maxR; i++) if (data[i][c] !== null) return false;
                return true;
            }};

            if (row1 === row2 && checkRowClear(row1, col1, col2)) return true;
            if (col1 === col2 && checkColClear(col1, row1, row2)) return true;
            if (data[row1][col2] === null && checkRowClear(row1, col1, col2) && checkColClear(col2, row1, row2)) return true;
            if (data[row2][col1] === null && checkColClear(col1, row1, row2) && checkRowClear(row2, col1, col2)) return true;

            for (let i = 0; i < rows; i++) {{
                if (i !== row1 && i !== row2 && data[i][col1] === null && data[i][col2] === null &&
                    checkColClear(col1, row1, i) && checkRowClear(i, col1, col2) && checkColClear(col2, i, row2)) return true;
            }}
            for (let j = 0; j < cols; j++) {{
                if (j !== col1 && j !== col2 && data[row1][j] === null && data[row2][j] === null &&
                    checkRowClear(row1, col1, j) && checkColClear(j, row1, row2) && checkRowClear(row2, j, col2)) return true;
            }}
            return false;
        }}

        isWin() {{
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) return false;
            return true;
        }}

        hasRemainingMoves() {{
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    if (this.gridData[i][j] !== null) {{
                        for (let m = 0; m < ROWS; m++) {{
                            for (let n = 0; n < COLS; n++) {{
                                if (i === m && j === n) continue;
                                if (this.gridData[m][n] !== null && this.gridData[i][j] === this.gridData[m][n] &&
                                    this.canConnect(i, j, m, n, this.gridData)) return true;
                            }}
                        }}
                    }}
                }}
            }}
            return false;
        }}

        // ---------- 操作 ----------
        shuffleBoard() {{
            let typeList = [];
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) typeList.push(this.gridData[i][j]);
            for (let i = typeList.length - 1; i > 0; i--) {{
                const j = Math.floor(Math.random() * (i + 1));
                [typeList[i], typeList[j]] = [typeList[j], typeList[i]];
            }}
            let idx = 0;
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) this.gridData[i][j] = typeList[idx++];
            this.renderGrid();
            this.clearSelected();
            this.showMessage("♻️ 重新洗牌啦", false);
            this.playCheerSound();
        }}

        giveHint() {{
            if (this.hintTimer) clearTimeout(this.hintTimer);
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    if (this.gridData[i][j] !== null) {{
                        for (let m = 0; m < ROWS; m++) {{
                            for (let n = 0; n < COLS; n++) {{
                                if (i === m && j === n) continue;
                                if (this.gridData[m][n] !== null && this.gridData[i][j] === this.gridData[m][n] &&
                                    this.canConnect(i, j, m, n, this.gridData)) {{
                                    this.highlightPair(i, j, m, n);
                                    this.showMessage(`💡 试试消除这只蝴蝶吧!`, false);
                                    this.playCheerSound();
                                    return;
                                }}
                            }}
                        }}
                    }}
                }}
            }}
            this.showMessage("🤔 没有可消除的,试试洗牌吧!", true);
        }}

        highlightPair(row1, col1, row2, col2) {{
            const cards = this.gridContainer.querySelectorAll('.card');
            const idx1 = row1 * COLS + col1;
            const idx2 = row2 * COLS + col2;
            if (cards[idx1] && cards[idx2]) {{
                cards[idx1].style.boxShadow = "0 0 0 2px gold, 0 0 0 5px #ffb347, 0 3px 0 rgba(0,0,0,0.2)";
                cards[idx2].style.boxShadow = "0 0 0 2px gold, 0 0 0 5px #ffb347, 0 3px 0 rgba(0,0,0,0.2)";
                this.hintTimer = setTimeout(() => this.renderGrid(), 1800);
            }}
        }}

        celebrateLevelCompletion() {{
            const message = `恭喜通关!当前总得分 ${{this.currentScore}} 分!`;
            this.playVictoryMusic();
            setTimeout(() => this.speak(message), 300);
            this.showMessage(`🎉🎉🎉 ${{message}} 🎉🎉🎉`, false);
            const cards = this.gridContainer.querySelectorAll('.card:not(.removed)');
            cards.forEach((card, index) => {{
                setTimeout(() => {{
                    card.classList.add('celebrate-card');
                    setTimeout(() => card.classList.remove('celebrate-card'), 500);
                }}, index * 30);
            }});
        }}

        eliminatePair(row1, col1, row2, col2) {{
            if (!this.canConnect(row1, col1, row2, col2, this.gridData)) return false;
            this.gridData[row1][col1] = null;
            this.gridData[row2][col2] = null;
            this.currentScore += 10;
            this.updateScoreUI();
            this.renderGrid();
            this.clearSelected();
            this.playCheerSound();
            this.showMessage(`🎉 +10分!消除成功!`, false);

            if (this.isWin()) {{
                this.celebrateLevelCompletion();
                setTimeout(() => {{
                    if (this.currentLevel < MAX_LEVEL) {{
                        this.currentLevel++;
                        this.updateLevelUI();
                        this.initLevelGrid();
                        this.clearSelected();
                        this.showMessage(`🎉 进入第 ${{this.currentLevel}} 关!难度提升啦~ 🎉`, false);
                        setTimeout(() => {{ if (!this.hasRemainingMoves()) this.shuffleBoard(); }}, 200);
                    }} else {{
                        this.playVictoryMusic();
                        this.speak("太厉害了!你已完成了全部10关挑战!");
                        this.showMessage(`🏆 恭喜你!已完成全部10关挑战!最终得分 ${{this.currentScore}} 分!`, false);
                    }}
                }}, 2000);
                return true;
            }}
            if (!this.hasRemainingMoves()) setTimeout(() => {{ if (!this.hasRemainingMoves()) this.shuffleBoard(); }}, 200);
            return true;
        }}

        // ---------- 渲染 ----------
        renderGrid() {{
            this.gridContainer.innerHTML = '';
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    const type = this.gridData[i][j];
                    const card = document.createElement('div');
                    card.className = 'card';
                    if (type === null) {{
                        card.classList.add('removed');
                    }} else {{
                        card.innerHTML = `<img src="${{BUTTERFLY_IMAGES[type]}}" alt="蝴蝶">`;
                    }}
                    if (this.selectedRow === i && this.selectedCol === j && type !== null) {{
                        card.classList.add('selected');
                    }}
                    card.addEventListener('click', (function(game, r, c) {{
                        return function() {{ game.onCardClick(r, c); }};
                    }})(this, i, j));
                    this.gridContainer.appendChild(card);
                }}
            }}
        }}

        onCardClick(row, col) {{
            if (this.gridData[row][col] === null) return;
            this.playCheerSound();

            if (this.selectedRow === null || this.selectedCol === null) {{
                this.selectedRow = row;
                this.selectedCol = col;
                this.renderGrid();
                this.showMessage(`选中了蝴蝶,再点击另一只相同蝴蝶消除~`, false);
                return;
            }}

            const sRow = this.selectedRow, sCol = this.selectedCol;
            if (sRow === row && sCol === col) {{
                this.clearSelected();
                this.showMessage("已取消选中", false);
                return;
            }}

            if (this.gridData[sRow][sCol] !== null && this.gridData[row][col] !== null &&
                this.gridData[sRow][sCol] === this.gridData[row][col] &&
                this.canConnect(sRow, sCol, row, col, this.gridData)) {{
                this.eliminatePair(sRow, sCol, row, col);
            }} else {{
                if (this.gridData[row][col] !== null) {{
                    if (this.gridData[sRow][sCol] !== this.gridData[row][col]) {{
                        this.showMessage(`🚫 两只蝴蝶不一样,不能消除哦`, true);
                    }} else {{
                        this.showMessage(`🚫 路径不通,最多拐两弯~`, true);
                    }}
                    this.selectedRow = row;
                    this.selectedCol = col;
                    this.renderGrid();
                }} else {{
                    this.clearSelected();
                }}
            }}
        }}

        // ---------- 初始化关卡(5列x6行,30格,保证成对) ----------
        initLevelGrid() {{
            let typesCount = Math.floor(4 + (this.currentLevel - 1) * (16 - 4) / (MAX_LEVEL - 1));
            typesCount = Math.min(typesCount, BUTTERFLY_IMAGES.length);

            let availableIndices = Array.from({{ length: BUTTERFLY_IMAGES.length }}, (_, i) => i);
            availableIndices.sort(() => Math.random() - 0.5);
            let selectedTypes = availableIndices.slice(0, typesCount);

            let pool = [];
            // 30格 / typesCount 的整数部分,保证偶数
            let baseCount = Math.floor(TOTAL_CELLS / typesCount);
            if (baseCount % 2 !== 0) baseCount -= 1;

            for (let i = 0; i < typesCount; i++) {{
                for (let k = 0; k < baseCount; k++) pool.push(selectedTypes[i]);
            }}

            let remainingCells = TOTAL_CELLS - pool.length;
            // 成对补充剩余格子
            while (remainingCells > 0) {{
                let randomType = selectedTypes[Math.floor(Math.random() * typesCount)];
                pool.push(randomType);
                pool.push(randomType);
                remainingCells -= 2;
            }}

            // 洗牌
            for (let i = pool.length - 1; i > 0; i--) {{
                const j = Math.floor(Math.random() * (i + 1));
                [pool[i], pool[j]] = [pool[j], pool[i]];
            }}

            this.gridData = Array(ROWS).fill().map(() => Array(COLS).fill(null));
            let idx = 0;
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) this.gridData[i][j] = pool[idx++];

            this.showMessage(`本关用了 ${{typesCount}} 种蝴蝶,加油!`, false);
            if (!this.hasRemainingMoves()) this.shuffleBoard();
            this.renderGrid();
        }}

        resetGame() {{
            this.currentLevel = 1;
            this.currentScore = 0;
            this.updateScoreUI();
            this.updateLevelUI();
            this.initLevelGrid();
            this.clearSelected();
            this.showMessage("🦋 10关挑战开始啦!", false);
            if ('speechSynthesis' in window) window.speechSynthesis.getVoices();
        }}
    }}

    // ==================== 启动三人游戏 ====================
    window.addEventListener('load', () => {{
        if (!BUTTERFLY_IMAGES || BUTTERFLY_IMAGES.length < 4) {{
            alert("图片数量太少,请确保 jpg 文件夹中有至少 4 张图片!");
            return;
        }}

        // 左侧游戏
        const gameLeft = new ButterflyGame('left', {{
            grid: 'gameGridLeft',
            score: 'scoreValueLeft',
            level: 'levelValueLeft',
            message: 'messageAreaLeft',
            shuffleBtn: 'shuffleBtnLeft',
            hintBtn: 'refreshHintBtnLeft'
        }});

        // 中间游戏
        const gameMiddle = new ButterflyGame('middle', {{
            grid: 'gameGridMiddle',
            score: 'scoreValueMiddle',
            level: 'levelValueMiddle',
            message: 'messageAreaMiddle',
            shuffleBtn: 'shuffleBtnMiddle',
            hintBtn: 'refreshHintBtnMiddle'
        }});

        // 右侧游戏
        const gameRight = new ButterflyGame('right', {{
            grid: 'gameGridRight',
            score: 'scoreValueRight',
            level: 'levelValueRight',
            message: 'messageAreaRight',
            shuffleBtn: 'shuffleBtnRight',
            hintBtn: 'refreshHintBtnRight'
        }});

        // 防滚动
        document.body.addEventListener('touchmove', e => e.preventDefault(), {{ passive: false }});
        document.body.addEventListener('wheel', e => e.preventDefault(), {{ passive: false }});

        if ('speechSynthesis' in window) window.speechSynthesis.getVoices();
    }});
</script>
</body>
</html>'''

    # 3. 写入文件
    try:
        os.makedirs(os.path.dirname(html_save_path), exist_ok=True)
        with open(html_save_path, 'w', encoding='utf-8') as f:
            f.write(html_content)
        print(f'✅ 三人对战版连连看生成成功!5列x6行,左右中独立,各10关。')
        print(f'📁 文件保存位置: {html_save_path}')
    except Exception as e:
        print(f'❌ 生成文件失败: {e}')
python 复制代码
'''
蝴蝶连连看 - 三人对战版(修复按钮文字显示)
自动扫描文件夹内的图片,压缩后转Base64,避免HTML过大卡死。
Deepseek,阿夏
20261108
'''

import os
import base64
from io import BytesIO

try:
    from PIL import Image
except ImportError:
    print('❌ 需要安装 Pillow:pip install Pillow')
    raise

def generate_image_based_game():
    # ================= 配置区 =================
    path = r'D:\000Python网页html\20261026蝴蝶连连看'
    img_folder = os.path.join(path, 'JPG')
    html_save_path = os.path.join(path, '011蝴蝶连连看_三人对战版_修复按钮.html')

    # 压缩参数
    MAX_SIZE = 320        # 最大边像素
    JPEG_QUALITY = 75     # JPEG质量
    # =========================================

    # 1. 检查并读取图片
    if not os.path.exists(img_folder):
        os.makedirs(img_folder)
        print(f'❌ 找不到文件夹 {img_folder}')
        print(f'✅ 已自动为您创建该文件夹,请把图片放进去,然后重新运行代码!')
        return

    valid_ext = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
    image_files = [
        os.path.join(img_folder, f)
        for f in os.listdir(img_folder)
        if f.lower().endswith(valid_ext)
    ]

    if len(image_files) == 0:
        print(f'❌ 在 {img_folder} 中没有找到任何图片文件,请检查!')
        return

    print(f'找到 {len(image_files)} 张图片,正在压缩并转换为离线数据...')

    base64_images = []
    for img_path in image_files:
        try:
            with Image.open(img_path) as im:
                if im.mode in ('RGBA', 'P', 'LA'):
                    im = im.convert('RGB')
                w, h = im.size
                if max(w, h) > MAX_SIZE:
                    scale = MAX_SIZE / max(w, h)
                    new_w, new_h = int(w * scale), int(h * scale)
                    im = im.resize((new_w, new_h), Image.LANCZOS)
                buf = BytesIO()
                im.save(buf, format='JPEG', quality=JPEG_QUALITY, optimize=True)
                b64_str = base64.b64encode(buf.getvalue()).decode('utf-8')
                base64_images.append(f"data:image/jpeg;base64,{b64_str}")
        except Exception as e:
            print(f"处理图片 {img_path} 失败: {e}")

    if not base64_images:
        print('❌ 图片处理失败,无法生成。')
        return

    js_images_array = '[\n' + ',\n'.join([f'"{img}"' for img in base64_images]) + '\n]'
    total_size = sum(len(img) for img in base64_images)
    print(f'压缩后Base64总大小:{total_size / 1024 / 1024:.2f} MB')

    # 2. 生成三人版 HTML 内容
    html_content = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>三人蝴蝶连连看 · 10关挑战版</title>
    <style>
        * {{
            user-select: none;
            -webkit-tap-highlight-color: transparent;
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }}

        html, body {{
            width: 100%;
            height: 100%;
            overflow: hidden;
            position: fixed;
            top: 0;
            left: 0;
            background: linear-gradient(145deg, #1b2a47 0%, #0d1321 100%);
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', 'Comic Neue', 'Comic Sans MS', 'Chalkboard SE', cursive, sans-serif;
            touch-action: none;
            padding: 4px;
        }}

        .triple-container {{
            display: flex;
            width: 100%;
            max-width: 1500px;
            height: 100%;
            max-height: 98vh;
            gap: 8px;
            justify-content: center;
            align-items: stretch;
        }}

        .player-panel {{
            flex: 1 1 0;
            min-width: 0;
            background: #fdf8e7;
            border-radius: 32px;
            box-shadow: 0 12px 20px rgba(0,0,0,0.4), inset 0 1px 3px rgba(255,255,200,0.8);
            padding: 8px 8px 12px 8px;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }}

        .player-title {{
            text-align: center;
            font-weight: bold;
            font-size: 0.95rem;
            padding: 3px 0 5px 0;
            color: #3d2a1a;
            letter-spacing: 1px;
            border-radius: 40px;
            margin-bottom: 4px;
            flex-shrink: 0;
        }}
        .player-left .player-title {{
            background: #ffd9b3;
            box-shadow: inset 0 1px 4px #ffe0b0, 0 3px 0 #c49a6c;
        }}
        .player-middle .player-title {{
            background: #c8e6c9;
            box-shadow: inset 0 1px 4px #d4f0d6, 0 3px 0 #7a9e7e;
        }}
        .player-right .player-title {{
            background: #b3e0ff;
            box-shadow: inset 0 1px 4px #cceeff, 0 3px 0 #6c9ac4;
        }}

        .info-panel {{
            display: flex;
            justify-content: space-between;
            align-items: baseline;
            background: #f5e5c8;
            padding: 4px 6px;
            border-radius: 60px;
            margin-bottom: 5px;
            box-shadow: inset 0 1px 4px rgba(0,0,0,0.05), 0 3px 6px rgba(0,0,0,0.1);
            flex-wrap: wrap;
            gap: 2px;
            flex-shrink: 0;
        }}

        .score-box, .level-box {{
            background: #5a3e2b;
            color: #ffefb9;
            padding: 2px 6px;
            border-radius: 40px;
            font-weight: bold;
            font-size: 0.7rem;
            text-shadow: 0 2px 0 #2e1e12;
            box-shadow: inset 0 1px 2px rgba(255,255,200,0.3), 0 2px 4px rgba(0,0,0,0.2);
            white-space: nowrap;
        }}

        .score-box span, .level-box span {{
            font-size: 0.85rem;
            margin-left: 2px;
            color: #ffd966;
        }}

        .grid-wrapper {{
            background: #fffaec;
            border-radius: 24px;
            padding: 5px;
            box-shadow: inset 0 0 0 2px #fff9e6, inset 0 0 0 3px #e7dbb8, 0 6px 10px rgba(0,0,0,0.15);
            flex: 1 1 auto;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 0;
        }}

        .game-grid {{
            display: grid;
            grid-template-columns: repeat(5, 1fr);
            gap: 4px;
            justify-items: center;
            align-items: center;
            width: 100%;
            height: 100%;
            aspect-ratio: 5 / 6;
            max-width: 100%;
            max-height: 100%;
        }}

        .card {{
            aspect-ratio: 1 / 1;
            width: 100%;
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: all 0.12s linear;
            box-shadow: 0 3px 0 rgba(0,0,0,0.2);
            transform: translateY(-1px);
            background-color: #fff;
            border: 1.5px solid #fff;
            min-width: 0;
            min-height: 0;
        }}

        .card img {{
            width: 90%;
            height: 90%;
            object-fit: contain;
            filter: drop-shadow(0 1px 1px rgba(0,0,0,0.15));
            transition: transform 0.3s;
            pointer-events: none;
        }}

        .card:hover img {{
            transform: scale(1.1);
        }}

        .card.selected {{
            box-shadow: 0 0 0 2px #ffb347, 0 0 0 5px #ffe484, 0 3px 0 rgba(0,0,0,0.2);
            transform: scale(0.95) translateY(0px);
        }}

        .card.removed {{
            visibility: hidden;
            pointer-events: none;
            box-shadow: none;
            transform: scale(0.8);
            background: transparent;
        }}

        /* 优化按钮区域,确保文字可见 */
        .action-buttons {{
            margin-top: 6px;
            display: flex;
            justify-content: center;
            gap: 8px;
            flex-shrink: 0;
        }}

        button {{
            background: #ffb347;
            border: none;
            font-family: inherit;
            font-weight: bold;
            font-size: 0.75rem;      /* 稍微调大字体 */
            padding: 0 12px;         /* 增加左右内边距,让文字有呼吸空间 */
            height: 28px;            /* 稍微增加高度 */
            min-width: 65px;         /* 关键:给文字留足最小宽度 */
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 14px;
            color: #3d2a1a;
            cursor: pointer;
            box-shadow: 0 2px 0 #a45d2e;
            transition: 0.07s linear;
            flex-shrink: 0;
            white-space: nowrap;     /* 防止文字换行被截断 */
        }}

        button:active {{
            transform: translateY(2px);
            box-shadow: 0 0px 0 #a45d2e;
        }}

        .message-area {{
            margin-top: 6px;
            background: #e9dbbc;
            border-radius: 24px;
            padding: 3px 6px;
            font-size: 0.7rem;
            font-weight: bold;
            color: #674e2c;
            min-height: 1.8rem;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            line-height: 1.2;
            flex-shrink: 0;
            word-break: break-word;
        }}

        @keyframes celebrate {{
            0% {{ transform: scale(1); }}
            50% {{ transform: scale(1.1); background: radial-gradient(circle, gold, #ffaa00); }}
            100% {{ transform: scale(1); }}
        }}

        .celebrate-card {{
            animation: celebrate 0.5s ease-in-out;
        }}

        /* 窄屏适配 */
        @media (max-width: 900px) {{
            .triple-container {{
                gap: 5px;
            }}
            .player-panel {{
                padding: 5px 4px 8px 4px;
                border-radius: 22px;
            }}
            .player-title {{
                font-size: 0.75rem;
                padding: 2px 0 3px 0;
            }}
            .score-box, .level-box {{
                font-size: 0.55rem;
                padding: 1px 4px;
            }}
            .score-box span, .level-box span {{
                font-size: 0.7rem;
            }}
            .game-grid {{
                gap: 3px;
            }}
            button {{
                min-width: 52px;
                height: 24px;
                font-size: 0.65rem;
                padding: 0 8px;
            }}
            .message-area {{
                font-size: 0.55rem;
                min-height: 1.5rem;
            }}
            .card {{
                border-radius: 7px;
            }}
        }}
    </style>
</head>
<body>
<div class="triple-container">
    <!-- 左侧玩家 -->
    <div class="player-panel player-left" id="playerLeft">
        <div class="player-title">🦋 左边</div>
        <div class="info-panel">
            <div class="score-box">🦋 <span id="scoreValueLeft">0</span></div>
            <div class="level-box">✨ <span id="levelValueLeft">1</span>/10</div>
        </div>
        <div class="grid-wrapper">
            <div class="game-grid" id="gameGridLeft"></div>
        </div>
        <div class="action-buttons">
            <button id="shuffleBtnLeft">🔄 洗牌</button>
            <button id="refreshHintBtnLeft">✨ 提示</button>
        </div>
        <div class="message-area" id="messageAreaLeft">
            🦋 开始啦~
        </div>
    </div>

    <!-- 中间玩家 -->
    <div class="player-panel player-middle" id="playerMiddle">
        <div class="player-title">🦋 中间</div>
        <div class="info-panel">
            <div class="score-box">🦋 <span id="scoreValueMiddle">0</span></div>
            <div class="level-box">✨ <span id="levelValueMiddle">1</span>/10</div>
        </div>
        <div class="grid-wrapper">
            <div class="game-grid" id="gameGridMiddle"></div>
        </div>
        <div class="action-buttons">
            <button id="shuffleBtnMiddle">🔄 洗牌</button>
            <button id="refreshHintBtnMiddle">✨ 提示</button>
        </div>
        <div class="message-area" id="messageAreaMiddle">
            🦋 开始啦~
        </div>
    </div>

    <!-- 右侧玩家 -->
    <div class="player-panel player-right" id="playerRight">
        <div class="player-title">🦋 右边</div>
        <div class="info-panel">
            <div class="score-box">🦋 <span id="scoreValueRight">0</span></div>
            <div class="level-box">✨ <span id="levelValueRight">1</span>/10</div>
        </div>
        <div class="grid-wrapper">
            <div class="game-grid" id="gameGridRight"></div>
        </div>
        <div class="action-buttons">
            <button id="shuffleBtnRight">🔄 洗牌</button>
            <button id="refreshHintBtnRight">✨ 提示</button>
        </div>
        <div class="message-area" id="messageAreaRight">
            🦋 开始啦~
        </div>
    </div>
</div>

<script>
    // ==================== 内嵌的高清蝴蝶图片数据 ====================
    const BUTTERFLY_IMAGES = {js_images_array};

    // ==================== 常量(5列x6行) ====================
    const COLS = 5;
    const ROWS = 6;
    const TOTAL_CELLS = ROWS * COLS;  // 30
    const MAX_LEVEL = 10;

    // ==================== 游戏类封装 ====================
    class ButterflyGame {{
        constructor(playerId, dom) {{
            this.playerId = playerId;
            this.dom = dom;

            this.gridContainer = document.getElementById(dom.grid);
            this.scoreSpan = document.getElementById(dom.score);
            this.levelSpan = document.getElementById(dom.level);
            this.messageDiv = document.getElementById(dom.message);
            this.shuffleBtn = document.getElementById(dom.shuffleBtn);
            this.hintBtn = document.getElementById(dom.hintBtn);

            this.currentLevel = 1;
            this.currentScore = 0;
            this.gridData = [];
            this.selectedRow = null;
            this.selectedCol = null;
            this.audioContext = null;
            this.hintTimer = null;

            this.shuffleBtn.addEventListener('click', () => this.shuffleBoard());
            this.hintBtn.addEventListener('click', () => this.giveHint());

            this.resetGame();
        }}

        // ---------- 音频 ----------
        playVictoryMusic() {{
            try {{
                if (!this.audioContext) {{
                    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    if (this.audioContext.state === 'suspended') this.audioContext.resume();
                }}
                const now = this.audioContext.currentTime;
                const notes = [523.25, 587.33, 659.25, 698.46, 783.99, 880.00, 987.77, 1046.50];
                notes.forEach((freq, index) => {{
                    const oscillator = this.audioContext.createOscillator();
                    const gainNode = this.audioContext.createGain();
                    oscillator.connect(gainNode);
                    gainNode.connect(this.audioContext.destination);
                    oscillator.frequency.value = freq;
                    oscillator.type = 'sine';
                    gainNode.gain.setValueAtTime(0.3, now + index * 0.15);
                    gainNode.gain.exponentialRampToValueAtTime(0.0001, now + (index * 0.15) + 0.3);
                    oscillator.start(now + index * 0.15);
                    oscillator.stop(now + (index * 0.15) + 0.3);
                }});
            }} catch (e) {{}}
        }}

        playCheerSound() {{
            try {{
                if (!this.audioContext) {{
                    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    if (this.audioContext.state === 'suspended') this.audioContext.resume();
                }}
                const now = this.audioContext.currentTime;
                const oscillator = this.audioContext.createOscillator();
                const gainNode = this.audioContext.createGain();
                oscillator.connect(gainNode);
                gainNode.connect(this.audioContext.destination);
                oscillator.frequency.value = 800;
                oscillator.type = 'sine';
                gainNode.gain.setValueAtTime(0.2, now);
                gainNode.gain.exponentialRampToValueAtTime(0.0001, now + 0.2);
                oscillator.start(now);
                oscillator.stop(now + 0.2);
            }} catch (e) {{}}
        }}

        speak(text) {{
            if ('speechSynthesis' in window) {{
                window.speechSynthesis.cancel();
                const utterance = new SpeechSynthesisUtterance(text);
                utterance.lang = 'zh-CN';
                utterance.rate = 0.9;
                utterance.pitch = 1.2;
                const voices = window.speechSynthesis.getVoices();
                const chineseVoice = voices.find(voice => voice.lang === 'zh-CN');
                if (chineseVoice) utterance.voice = chineseVoice;
                window.speechSynthesis.speak(utterance);
            }}
        }}

        // ---------- UI ----------
        updateScoreUI() {{ this.scoreSpan.innerText = this.currentScore; }}
        updateLevelUI() {{ this.levelSpan.innerText = this.currentLevel; }}

        showMessage(msg, isError = false) {{
            this.messageDiv.innerHTML = `${{isError ? '⚠️ ' : '🍬 '}} ${{msg}}`;
        }}

        clearSelected() {{
            this.selectedRow = null;
            this.selectedCol = null;
            this.renderGrid();
        }}

        // ---------- 连通判断(支持5列x6行) ----------
        canConnect(row1, col1, row2, col2, data) {{
            if (row1 === row2 && col1 === col2) return false;
            if (data[row1][col1] === null || data[row2][col2] === null) return false;
            if (data[row1][col1] !== data[row2][col2]) return false;

            const rows = data.length;
            const cols = data[0].length;

            const checkRowClear = (r, cA, cB) => {{
                let minC = Math.min(cA, cB), maxC = Math.max(cA, cB);
                for (let i = minC + 1; i < maxC; i++) if (data[r][i] !== null) return false;
                return true;
            }};
            const checkColClear = (c, rA, rB) => {{
                let minR = Math.min(rA, rB), maxR = Math.max(rA, rB);
                for (let i = minR + 1; i < maxR; i++) if (data[i][c] !== null) return false;
                return true;
            }};

            if (row1 === row2 && checkRowClear(row1, col1, col2)) return true;
            if (col1 === col2 && checkColClear(col1, row1, row2)) return true;
            if (data[row1][col2] === null && checkRowClear(row1, col1, col2) && checkColClear(col2, row1, row2)) return true;
            if (data[row2][col1] === null && checkColClear(col1, row1, row2) && checkRowClear(row2, col1, col2)) return true;

            for (let i = 0; i < rows; i++) {{
                if (i !== row1 && i !== row2 && data[i][col1] === null && data[i][col2] === null &&
                    checkColClear(col1, row1, i) && checkRowClear(i, col1, col2) && checkColClear(col2, i, row2)) return true;
            }}
            for (let j = 0; j < cols; j++) {{
                if (j !== col1 && j !== col2 && data[row1][j] === null && data[row2][j] === null &&
                    checkRowClear(row1, col1, j) && checkColClear(j, row1, row2) && checkRowClear(row2, j, col2)) return true;
            }}
            return false;
        }}

        isWin() {{
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) return false;
            return true;
        }}

        hasRemainingMoves() {{
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    if (this.gridData[i][j] !== null) {{
                        for (let m = 0; m < ROWS; m++) {{
                            for (let n = 0; n < COLS; n++) {{
                                if (i === m && j === n) continue;
                                if (this.gridData[m][n] !== null && this.gridData[i][j] === this.gridData[m][n] &&
                                    this.canConnect(i, j, m, n, this.gridData)) return true;
                            }}
                        }}
                    }}
                }}
            }}
            return false;
        }}

        // ---------- 操作 ----------
        shuffleBoard() {{
            let typeList = [];
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) typeList.push(this.gridData[i][j]);
            for (let i = typeList.length - 1; i > 0; i--) {{
                const j = Math.floor(Math.random() * (i + 1));
                [typeList[i], typeList[j]] = [typeList[j], typeList[i]];
            }}
            let idx = 0;
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) if (this.gridData[i][j] !== null) this.gridData[i][j] = typeList[idx++];
            this.renderGrid();
            this.clearSelected();
            this.showMessage("♻️ 重新洗牌啦", false);
            this.playCheerSound();
        }}

        giveHint() {{
            if (this.hintTimer) clearTimeout(this.hintTimer);
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    if (this.gridData[i][j] !== null) {{
                        for (let m = 0; m < ROWS; m++) {{
                            for (let n = 0; n < COLS; n++) {{
                                if (i === m && j === n) continue;
                                if (this.gridData[m][n] !== null && this.gridData[i][j] === this.gridData[m][n] &&
                                    this.canConnect(i, j, m, n, this.gridData)) {{
                                    this.highlightPair(i, j, m, n);
                                    this.showMessage(`💡 试试消除这只蝴蝶吧!`, false);
                                    this.playCheerSound();
                                    return;
                                }}
                            }}
                        }}
                    }}
                }}
            }}
            this.showMessage("🤔 没有可消除的,试试洗牌吧!", true);
        }}

        highlightPair(row1, col1, row2, col2) {{
            const cards = this.gridContainer.querySelectorAll('.card');
            const idx1 = row1 * COLS + col1;
            const idx2 = row2 * COLS + col2;
            if (cards[idx1] && cards[idx2]) {{
                cards[idx1].style.boxShadow = "0 0 0 2px gold, 0 0 0 5px #ffb347, 0 3px 0 rgba(0,0,0,0.2)";
                cards[idx2].style.boxShadow = "0 0 0 2px gold, 0 0 0 5px #ffb347, 0 3px 0 rgba(0,0,0,0.2)";
                this.hintTimer = setTimeout(() => this.renderGrid(), 1800);
            }}
        }}

        celebrateLevelCompletion() {{
            const message = `恭喜通关!当前总得分 ${{this.currentScore}} 分!`;
            this.playVictoryMusic();
            setTimeout(() => this.speak(message), 300);
            this.showMessage(`🎉🎉🎉 ${{message}} 🎉🎉🎉`, false);
            const cards = this.gridContainer.querySelectorAll('.card:not(.removed)');
            cards.forEach((card, index) => {{
                setTimeout(() => {{
                    card.classList.add('celebrate-card');
                    setTimeout(() => card.classList.remove('celebrate-card'), 500);
                }}, index * 30);
            }});
        }}

        eliminatePair(row1, col1, row2, col2) {{
            if (!this.canConnect(row1, col1, row2, col2, this.gridData)) return false;
            this.gridData[row1][col1] = null;
            this.gridData[row2][col2] = null;
            this.currentScore += 10;
            this.updateScoreUI();
            this.renderGrid();
            this.clearSelected();
            this.playCheerSound();
            this.showMessage(`🎉 +10分!消除成功!`, false);

            if (this.isWin()) {{
                this.celebrateLevelCompletion();
                setTimeout(() => {{
                    if (this.currentLevel < MAX_LEVEL) {{
                        this.currentLevel++;
                        this.updateLevelUI();
                        this.initLevelGrid();
                        this.clearSelected();
                        this.showMessage(`🎉 进入第 ${{this.currentLevel}} 关!难度提升啦~ 🎉`, false);
                        setTimeout(() => {{ if (!this.hasRemainingMoves()) this.shuffleBoard(); }}, 200);
                    }} else {{
                        this.playVictoryMusic();
                        this.speak("太厉害了!你已完成了全部10关挑战!");
                        this.showMessage(`🏆 恭喜你!已完成全部10关挑战!最终得分 ${{this.currentScore}} 分!`, false);
                    }}
                }}, 2000);
                return true;
            }}
            if (!this.hasRemainingMoves()) setTimeout(() => {{ if (!this.hasRemainingMoves()) this.shuffleBoard(); }}, 200);
            return true;
        }}

        // ---------- 渲染 ----------
        renderGrid() {{
            this.gridContainer.innerHTML = '';
            for (let i = 0; i < ROWS; i++) {{
                for (let j = 0; j < COLS; j++) {{
                    const type = this.gridData[i][j];
                    const card = document.createElement('div');
                    card.className = 'card';
                    if (type === null) {{
                        card.classList.add('removed');
                    }} else {{
                        card.innerHTML = `<img src="${{BUTTERFLY_IMAGES[type]}}" alt="蝴蝶">`;
                    }}
                    if (this.selectedRow === i && this.selectedCol === j && type !== null) {{
                        card.classList.add('selected');
                    }}
                    card.addEventListener('click', (function(game, r, c) {{
                        return function() {{ game.onCardClick(r, c); }};
                    }})(this, i, j));
                    this.gridContainer.appendChild(card);
                }}
            }}
        }}

        onCardClick(row, col) {{
            if (this.gridData[row][col] === null) return;
            this.playCheerSound();

            if (this.selectedRow === null || this.selectedCol === null) {{
                this.selectedRow = row;
                this.selectedCol = col;
                this.renderGrid();
                this.showMessage(`选中了蝴蝶,再点击另一只相同蝴蝶消除~`, false);
                return;
            }}

            const sRow = this.selectedRow, sCol = this.selectedCol;
            if (sRow === row && sCol === col) {{
                this.clearSelected();
                this.showMessage("已取消选中", false);
                return;
            }}

            if (this.gridData[sRow][sCol] !== null && this.gridData[row][col] !== null &&
                this.gridData[sRow][sCol] === this.gridData[row][col] &&
                this.canConnect(sRow, sCol, row, col, this.gridData)) {{
                this.eliminatePair(sRow, sCol, row, col);
            }} else {{
                if (this.gridData[row][col] !== null) {{
                    if (this.gridData[sRow][sCol] !== this.gridData[row][col]) {{
                        this.showMessage(`🚫 两只蝴蝶不一样,不能消除哦`, true);
                    }} else {{
                        this.showMessage(`🚫 路径不通,最多拐两弯~`, true);
                    }}
                    this.selectedRow = row;
                    this.selectedCol = col;
                    this.renderGrid();
                }} else {{
                    this.clearSelected();
                }}
            }}
        }}

        // ---------- 初始化关卡(5列x6行,30格,保证成对) ----------
        initLevelGrid() {{
            let typesCount = Math.floor(4 + (this.currentLevel - 1) * (16 - 4) / (MAX_LEVEL - 1));
            typesCount = Math.min(typesCount, BUTTERFLY_IMAGES.length);

            let availableIndices = Array.from({{ length: BUTTERFLY_IMAGES.length }}, (_, i) => i);
            availableIndices.sort(() => Math.random() - 0.5);
            let selectedTypes = availableIndices.slice(0, typesCount);

            let pool = [];
            // 30格 / typesCount 的整数部分,保证偶数
            let baseCount = Math.floor(TOTAL_CELLS / typesCount);
            if (baseCount % 2 !== 0) baseCount -= 1;

            for (let i = 0; i < typesCount; i++) {{
                for (let k = 0; k < baseCount; k++) pool.push(selectedTypes[i]);
            }}

            let remainingCells = TOTAL_CELLS - pool.length;
            // 成对补充剩余格子
            while (remainingCells > 0) {{
                let randomType = selectedTypes[Math.floor(Math.random() * typesCount)];
                pool.push(randomType);
                pool.push(randomType);
                remainingCells -= 2;
            }}

            // 洗牌
            for (let i = pool.length - 1; i > 0; i--) {{
                const j = Math.floor(Math.random() * (i + 1));
                [pool[i], pool[j]] = [pool[j], pool[i]];
            }}

            this.gridData = Array(ROWS).fill().map(() => Array(COLS).fill(null));
            let idx = 0;
            for (let i = 0; i < ROWS; i++) for (let j = 0; j < COLS; j++) this.gridData[i][j] = pool[idx++];

            this.showMessage(`本关用了 ${{typesCount}} 种蝴蝶,加油!`, false);
            if (!this.hasRemainingMoves()) this.shuffleBoard();
            this.renderGrid();
        }}

        resetGame() {{
            this.currentLevel = 1;
            this.currentScore = 0;
            this.updateScoreUI();
            this.updateLevelUI();
            this.initLevelGrid();
            this.clearSelected();
            this.showMessage("🦋 10关挑战开始啦!", false);
            if ('speechSynthesis' in window) window.speechSynthesis.getVoices();
        }}
    }}

    // ==================== 启动三人游戏 ====================
    window.addEventListener('load', () => {{
        if (!BUTTERFLY_IMAGES || BUTTERFLY_IMAGES.length < 4) {{
            alert("图片数量太少,请确保 jpg 文件夹中有至少 4 张图片!");
            return;
        }}

        // 左侧游戏
        const gameLeft = new ButterflyGame('left', {{
            grid: 'gameGridLeft',
            score: 'scoreValueLeft',
            level: 'levelValueLeft',
            message: 'messageAreaLeft',
            shuffleBtn: 'shuffleBtnLeft',
            hintBtn: 'refreshHintBtnLeft'
        }});

        // 中间游戏
        const gameMiddle = new ButterflyGame('middle', {{
            grid: 'gameGridMiddle',
            score: 'scoreValueMiddle',
            level: 'levelValueMiddle',
            message: 'messageAreaMiddle',
            shuffleBtn: 'shuffleBtnMiddle',
            hintBtn: 'refreshHintBtnMiddle'
        }});

        // 右侧游戏
        const gameRight = new ButterflyGame('right', {{
            grid: 'gameGridRight',
            score: 'scoreValueRight',
            level: 'levelValueRight',
            message: 'messageAreaRight',
            shuffleBtn: 'shuffleBtnRight',
            hintBtn: 'refreshHintBtnRight'
        }});

        // 防滚动
        document.body.addEventListener('touchmove', e => e.preventDefault(), {{ passive: false }});
        document.body.addEventListener('wheel', e => e.preventDefault(), {{ passive: false }});

        if ('speechSynthesis' in window) window.speechSynthesis.getVoices();
    }});
</script>
</body>
</html>'''

    # 3. 写入文件
    try:
        os.makedirs(os.path.dirname(html_save_path), exist_ok=True)
        with open(html_save_path, 'w', encoding='utf-8') as f:
            f.write(html_content)
        file_size = os.path.getsize(html_save_path) / 1024 / 1024
        print(f'✅ 三人对战版连连看生成成功!按钮已修复。')
        print(f'📁 文件保存位置: {html_save_path}')
        print(f'📦 HTML文件大小:{file_size:.2f} MB')
    except Exception as e:
        print(f'❌ 生成文件失败: {e}')

if __name__ == '__main__':
    generate_image_based_game()
相关推荐
用户0190275816124 分钟前
用 Polars 快速分析 A 股 K 线:比 pandas 更快的列式处理
python
用户03321266636732 分钟前
使用 Python 轻松加密或解密 Excel 文档
python·excel
CyrusYiVPP1 小时前
手写 MCP Server:给问答机器人接上本地文件工具,Python 实战第 13 期
人工智能·python
特立独行的猫A2 小时前
cj-tauri:用仓颉写桌面与鸿蒙 PC 应用
java·后端·python
qq_1588954111110 小时前
AI 音乐生成完成后,音频 URL 为什么会过期?一套可恢复的交付方案
python·api·ai音乐
Warson_L13 小时前
高性能架构设计
python·go
databook13 小时前
新数据到手,先跑哪几步检查
python·数据分析
czq_268671948714 小时前
Python打卡第46天
开发语言·python
Flynt15 小时前
日涨4000星的OSINT工具,我拿Linus的用户名和一个随机字符串实测了一遍
python·安全