一、背景需求
前期已经做了单页面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个页面)
'''
蝴蝶连连看 - 双人对战版(左右独立,各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列。
'''
蝴蝶连连看 - 三人对战版(左中右独立,各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}')
'''
蝴蝶连连看 - 三人对战版(修复按钮文字显示)
自动扫描文件夹内的图片,压缩后转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()