import React, { useState, useEffect, useCallback } from 'react'; // --- 游戏基础配置 --- const BOARD_SIZE = 8; const CANDY_TYPES = ['🍎', '🍊', '🍇', '🍉', '🥝', '🫐']; // --- 自动生成 20 个关卡的配置 --- const LEVELS = Array.from({ length: 20 }, (_, i) => ({ id: i + 1, targetScore: 1000 + i * 800, // 目标分数逐渐增加 moves: 15 + Math.floor(i / 2), // 步数根据关卡稍微增加,但难度比例变高 })); // --- 核心逻辑函数 (脱离组件避免重复渲染问题) --- // 检查并获取所有匹配的索引 (横向和纵向) const getMatches = (currentBoard) => { const matches = new Set(); // 检查横向 for (let r = 0; r < BOARD_SIZE; r++) { for (let c = 0; c < BOARD_SIZE - 2; c++) { let idx1 = r * BOARD_SIZE + c; let idx2 = r * BOARD_SIZE + c + 1; let idx3 = r * BOARD_SIZE + c + 2; if ( currentBoard[idx1] && currentBoard[idx1] === currentBoard[idx2] && currentBoard[idx1] === currentBoard[idx3] ) { matches.add(idx1).add(idx2).add(idx3); // 继续向右检查是否有更长的连消 (4消,5消等) let k = 3; while (c + k < BOARD_SIZE && currentBoard[idx1] === currentBoard[r * BOARD_SIZE + c + k]) { matches.add(r * BOARD_SIZE + c + k); k++; } } } } // 检查纵向 for (let c = 0; c < BOARD_SIZE; c++) { for (let r = 0; r < BOARD_SIZE - 2; r++) { let idx1 = r * BOARD_SIZE + c; let idx2 = (r + 1) * BOARD_SIZE + c; let idx3 = (r + 2) * BOARD_SIZE + c; if ( currentBoard[idx1] && currentBoard[idx1] === currentBoard[idx2] && currentBoard[idx1] === currentBoard[idx3] ) { matches.add(idx1).add(idx2).add(idx3); // 继续向下检查是否有更长的连消 let k = 3; while (r + k < BOARD_SIZE && currentBoard[idx1] === currentBoard[(r + k) * BOARD_SIZE + c]) { matches.add((r + k) * BOARD_SIZE + c); k++; } } } } return Array.from(matches); }; // 生成一个没有初始匹配项的合法开局棋盘 const generateValidBoard = () => { let board = new Array(BOARD_SIZE * BOARD_SIZE).fill(null); for (let i = 0; i < board.length; i++) { let available = [...CANDY_TYPES].sort(() => Math.random() - 0.5); // 打乱顺序 for (let type of available) { board[i] = type; // 临时检查放入该项后是否会产生匹配,如果不产生,则确认使用该水果 if (getMatches(board).length === 0) { break; } } } return board; }; // 处理重力掉落和填充新水果 const applyGravity = (currentBoard) => { let newBoard = [...currentBoard]; for (let c = 0; c < BOARD_SIZE; c++) { let col = []; // 从下往上提取非空的元素 for (let r = BOARD_SIZE - 1; r >= 0; r--) { if (newBoard[r * BOARD_SIZE + c] !== null) { col.push(newBoard[r * BOARD_SIZE + c]); } } // 顶部缺失的位置用新生成的随机水果填补 while (col.length < BOARD_SIZE) { col.push(CANDY_TYPES[Math.floor(Math.random() * CANDY_TYPES.length)]); } // 将提取和生成的一列重新放回棋盘 for (let r = BOARD_SIZE - 1; r >= 0; r--) { newBoard[r * BOARD_SIZE + c] = col[(BOARD_SIZE - 1) - r]; } } return newBoard; }; // 检查两个方块是否相邻 const checkAdjacent = (idx1, idx2) => { const r1 = Math.floor(idx1 / BOARD_SIZE); const c1 = idx1 % BOARD_SIZE; const r2 = Math.floor(idx2 / BOARD_SIZE); const c2 = idx2 % BOARD_SIZE; return (Math.abs(r1 - r2) === 1 && c1 === c2) || (Math.abs(c1 - c2) === 1 && r1 === r2); }; export default function App() { // --- 状态管理 --- const [maxLevel, setMaxLevel] = useState(1); const [gameState, setGameState] = useState('menu'); // 'menu', 'playing', 'won', 'lost' const [currentLevel, setCurrentLevel] = useState(null); const [board, setBoard] = useState([]); const [score, setScore] = useState(0); const [moves, setMoves] = useState(0); const [selectedCell, setSelectedCell] = useState(null); const [isAnimating, setIsAnimating] = useState(false); const [isUserSwapping, setIsUserSwapping] = useState(false); const [clearedIndices, setClearedIndices] = useState([]); // 初始化读取本地最高关卡记录 useEffect(() => { try { const savedMax = localStorage.getItem('match3_maxLevel'); if (savedMax) setMaxLevel(parseInt(savedMax, 10)); } catch (e) { console.warn('Local storage disabled'); } }, []); // 注入自定义动画 CSS useEffect(() => { const style = document.createElement('style'); style.innerHTML = ` @keyframes popAnim { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.8; } 100% { transform: scale(0); opacity: 0; } } .anim-pop { animation: popAnim 0.3s ease-out forwards; } `; document.head.appendChild(style); return () => document.head.removeChild(style); }, []); // --- 游戏控制 --- const startGame = (levelObj) => { setCurrentLevel(levelObj); setScore(0); setMoves(levelObj.moves); setBoard(generateValidBoard()); setClearedIndices([]); setSelectedCell(null); setIsAnimating(false); setIsUserSwapping(false); setGameState('playing'); }; const handleLevelUnlock = (levelId) => { const nextMax = Math.max(maxLevel, levelId + 1); setMaxLevel(nextMax); try { localStorage.setItem('match3_maxLevel', nextMax.toString()); } catch (e) { } }; // --- 核心交互与连锁反应 --- // 尝试交换两个方块 const trySwap = (idx1, idx2) => { setIsUserSwapping(true); let b = [...board]; // 执行交换 [b[idx1], b[idx2]] = [b[idx2], b[idx1]]; setBoard(b); // 延迟一点时间检查是否合法,产生视觉上的交互反馈 setTimeout(() => { const matches = getMatches(b); if (matches.length > 0) { // 合法交换:扣除步数,连消循环将由 useEffect 接管 setMoves((m) => m - 1); setIsUserSwapping(false); } else { // 非法交换:回退位置 let revertB = [...b]; [revertB[idx1], revertB[idx2]] = [revertB[idx2], revertB[idx1]]; setBoard(revertB); setIsUserSwapping(false); } }, 300); }; // 处理方块点击 const handleCellClick = (index) => { if (gameState !== 'playing' || isAnimating || isUserSwapping) return; if (selectedCell === null) { setSelectedCell(index); } else { if (selectedCell === index) { setSelectedCell(null); // 取消选中 } else if (checkAdjacent(selectedCell, index)) { trySwap(selectedCell, index); setSelectedCell(null); } else { setSelectedCell(index); // 点击了不相邻的,直接改选该方块 } } }; // 自动检测消除与掉落的连消循环 (级联效果) useEffect(() => { if (gameState !== 'playing' || isUserSwapping) return; const matches = getMatches(board); if (matches.length > 0) { setIsAnimating(true); setClearedIndices(matches); // 第一步:播放消除动画,计算分数 const timer = setTimeout(() => { let b = [...board]; matches.forEach(i => b[i] = null); // 计算得分:基础每块10分,大于3连有额外加成 const newPoints = matches.length * 10 + (matches.length > 3 ? (matches.length - 3) * 20 : 0); setScore(s => s + newPoints); // 第二步:执行重力掉落并填充新水果 const nextBoard = applyGravity(b); setClearedIndices([]); setBoard(nextBoard); // 这会触发下一个 useEffect,从而实现无限连消检查 }, 300); return () => clearTimeout(timer); } else { setIsAnimating(false); // 当盘面稳定没有可消项时,判定输赢 if (currentLevel) { if (score >= currentLevel.targetScore) { setGameState('won'); handleLevelUnlock(currentLevel.id); } else if (moves <= 0) { setGameState('lost'); } } } }, [board, gameState, isUserSwapping, score, moves, currentLevel]); // --- 视图渲染组件 --- // 1. 关卡菜单视图 if (gameState === 'menu') { return (

🍉 开心消消乐 🍇

{LEVELS.map((lvl) => { const isLocked = lvl.id > maxLevel; return ( ); })}
提示:滑动(点击)相邻的水果使3个以上相连即可消除得分。
); } // 2. 游戏主视图 return (
{/* 顶部状态栏 */}
第 {currentLevel.id} 关
{score} / {currentLevel.targetScore}
剩余步数
{moves}
{/* 游戏棋盘 */}
{board.map((item, index) => { const isSelected = selectedCell === index; const isCleared = clearedIndices.includes(index); return (
handleCellClick(index)} className={` aspect-square rounded-xl flex items-center justify-center text-3xl sm:text-4xl cursor-pointer transition-transform duration-200 ${item ? 'bg-white/10' : 'bg-transparent'} ${isSelected ? 'ring-4 ring-yellow-400 scale-110 z-10 bg-white/30' : ''} ${isCleared ? 'anim-pop' : ''} hover:bg-white/20 `} >
{item}
); })}
{/* 交互遮罩:防止动画播放时玩家点击 */} {(isAnimating || isUserSwapping) && (
)}
{/* 底部按钮 */}
{/* 结算弹窗 (胜利/失败) */} {(gameState === 'won' || gameState === 'lost') && (
{gameState === 'won' ? '🎉' : '💔'}

{gameState === 'won' ? '闯关成功!' : '步数用尽!'}

最终得分: {score}

{gameState === 'won' && currentLevel.id < 20 ? ( ) : gameState === 'won' && currentLevel.id === 20 ? (
🏆 您已通关全部20关!
) : ( )}
)}
); }