连连看 - Electron for 鸿蒙PC项目实战案例

项目概述

这是一个基于Electron for 鸿蒙PC的连连看游戏实现,专为初学者设计。连连看是一款经典的配对消除游戏,玩家需要找出相同的两个图案,并通过最多三条直线连接它们进行消除。游戏锻炼玩家的观察力和反应速度。
在这里插入图片描述

技术要点

核心技术

  • 路径查找算法:实现连连看的路径搜索算法,支持0弯、1弯、2弯和3弯路径检测
  • 图像渲染:加载和显示游戏图案,实现方块的消除动画
  • 游戏逻辑控制:管理游戏流程、计分系统和难度设置
  • DOM操作:动态创建和管理游戏元素

主要功能

  • 图案匹配:找出相同的图案并进行消除
  • 连线显示:直观显示匹配图案之间的连接路径
  • 倒计时系统:设置游戏时间限制,增加挑战性
  • 分数系统:根据消除速度和难度计算分数
  • 难度调节:提供不同的游戏难度级别
  • 提示功能:在玩家困难时提供匹配提示
  • 重新洗牌:当无法找到匹配时,可以重新排列图案

Electron特性应用

  • 使用Menu模块创建自定义应用菜单,提供游戏控制选项
  • 使用ipcMainipcRenderer进行进程间通信
  • 使用dialog模块显示游戏设置和结果对话框
  • 使用globalShortcut注册快捷键,支持键盘操作游戏

项目结构

17-link-game/
├── main.js         # Electron主进程
├── index.html      # 游戏主界面
├── preload.js      # 预加载脚本
├── renderer.js     # 渲染进程逻辑
├── style.css       # 游戏样式
├── package.json    # 项目配置
└── README.md       # 项目说明

实现细节

游戏核心逻辑

  • 棋盘初始化:随机生成游戏图案,确保每种图案都有且仅有两个
  • 路径检测算法
    • 直线检测:检查两个方块是否在同一行或同一列,且中间无阻挡
    • 一弯检测:通过一个转折点连接两个方块
    • 两弯检测:通过两个转折点连接两个方块
  • 消除处理:当找到有效路径时,移除匹配的方块并更新游戏状态
  • 胜利条件:当所有方块都被消除时,游戏胜利
  • 失败条件:当时间用完或无法继续消除且无法重新洗牌时,游戏失败

render.js代码示例

// 导入electron API
const { electronAPI } = window;

// DOM元素
let gameBoard;
let cardsLeftElement;
let timerElement;
let movesElement;
let hintsLeftElement;
let totalGamesElement;
let winsCountElement;
let winRateElement;
let bestTimeElement;
let connectionLinesElement;
let loadingIndicator;
let toastElement;
let modals = {};

// 游戏状态
let gameState = {
  cards: [],
  selectedCard: null,
  disabledCards: false,
  moves: 0,
  timer: 0,
  timerInterval: null,
  gameStarted: false,
  hintsLeft: 3,
  totalHintsUsed: 0,
  difficulty: 'medium', // easy, medium, hard
  history: [],
  settings: {
    sound: true,
    musicVolume: 80,
    soundVolume: 80,
    showGrid: true,
    autoHint: false,
    hintCount: 3
  },
  statistics: {
    totalGames: 0,
    wins: 0,
    bestTime: null
  },
  boardSize: {
    rows: 6,
    cols: 6
  },
  cardSize: 60,
  cardGap: 5,
  cardIcons: ['🎯', '🎲', '🎨', '🎸', '🎮', '🎪', '🎭', '🎨', 
              '🎯', '🎲', '🎸', '🎮', '🎪', '🎭', '🔮', '🌟', 
              '🎨', '🎯', '🎲', '🎸', '🎮', '🎪', '🎭', '🔮', 
              '🌟', '🎨', '🎯', '🎲', '🎸', '🎮', '🎪', '🎭']
};

// 初始化游戏
function initGame() {
  // 获取DOM元素
  gameBoard = document.getElementById('game-board');
  cardsLeftElement = document.getElementById('cards-left');
  timerElement = document.getElementById('timer');
  movesElement = document.getElementById('moves');
  hintsLeftElement = document.getElementById('hints-left');
  totalGamesElement = document.getElementById('total-games');
  winsCountElement = document.getElementById('wins-count');
  winRateElement = document.getElementById('win-rate');
  bestTimeElement = document.getElementById('best-time');
  connectionLinesElement = document.getElementById('connection-lines');
  loadingIndicator = document.getElementById('loading-indicator');
  toastElement = document.getElementById('toast');
  
  // 获取模态窗口
  modals.gameOver = document.getElementById('game-over-modal');
  modals.settings = document.getElementById('settings-modal');
  modals.rules = document.getElementById('rules-modal');
  modals.shortcuts = document.getElementById('shortcuts-modal');
  modals.about = document.getElementById('about-modal');
  
  // 加载游戏设置和统计数据
  loadGameData();
  
  // 设置事件监听器
  setupEventListeners();
  
  // 调整游戏棋盘大小
  resizeGameBoard();
  
  // 创建新游戏
  createNewGame();
  
  // 隐藏加载指示器
  setTimeout(() => {
    loadingIndicator.style.display = 'none';
  }, 500);
}

// 加载游戏数据
function loadGameData() {
  try {
    const savedSettings = localStorage.getItem('linkGameSettings');
    if (savedSettings) {
      gameState.settings = { ...gameState.settings, ...JSON.parse(savedSettings) };
    }
    
    const savedStats = localStorage.getItem('linkGameStatistics');
    if (savedStats) {
      gameState.statistics = { ...gameState.statistics, ...JSON.parse(savedStats) };
      updateStatisticsDisplay();
    }
    
    // 应用主题
    const savedTheme = localStorage.getItem('linkGameTheme') || 'light';
    document.body.dataset.theme = savedTheme;
    
    // 应用设置
    applySettings();
  } catch (error) {
    console.error('加载游戏数据失败:', error);
  }
}

// 保存游戏数据
function saveGameData() {
  try {
    localStorage.setItem('linkGameSettings', JSON.stringify(gameState.settings));
    localStorage.setItem('linkGameStatistics', JSON.stringify(gameState.statistics));
  } catch (error) {
    console.error('保存游戏数据失败:', error);
  }
}

// 应用设置
function applySettings() {
  // 应用提示次数
  gameState.hintsLeft = gameState.settings.hintCount;
  hintsLeftElement.textContent = gameState.hintsLeft;
  
  // 应用网格线设置
  if (gameState.settings.showGrid) {
    gameBoard.style.borderCollapse = 'separate';
  } else {
    gameBoard.style.borderCollapse = 'collapse';
  }
}

// 设置事件监听器
function setupEventListeners() {
  // 游戏控制按钮
  document.getElementById('new-game-btn').addEventListener('click', createNewGame);
  document.getElementById('hint-btn').addEventListener('click', showHint);
  document.getElementById('shuffle-btn').addEventListener('click', shuffleCards);
  document.getElementById('undo-btn').addEventListener('click', undoLastMove);
  
  // 难度按钮
  document.querySelectorAll('.difficulty-buttons .btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const difficulty = btn.dataset.difficulty;
      changeDifficulty(difficulty);
      createNewGame();
    });
  });
  
  // 主题切换
  document.querySelectorAll('.theme-buttons .btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const theme = btn.dataset.theme;
      document.body.dataset.theme = theme;
      localStorage.setItem('linkGameTheme', theme);
    });
  });
  
  // 设置按钮
  document.getElementById('settings-btn').addEventListener('click', () => {
    openModal('settings');
    updateSettingsForm();
  });
  
  // 规则按钮
  document.getElementById('rules-btn').addEventListener('click', () => {
    openModal('rules');
  });
  
  // 快捷键按钮
  document.getElementById('shortcuts-btn').addEventListener('click', () => {
    openModal('shortcuts');
  });
  
  // 关于按钮
  document.getElementById('about-btn').addEventListener('click', () => {
    openModal('about');
  });
  
  // 关闭模态窗口按钮
  document.querySelectorAll('.modal-close').forEach(btn => {
    btn.addEventListener('click', () => {
      closeModal();
    });
  });
  
  // 游戏结束模态窗口按钮
  document.getElementById('play-again-btn').addEventListener('click', () => {
    closeModal();
    createNewGame();
  });
  
  document.getElementById('change-difficulty-btn').addEventListener('click', () => {
    closeModal();
    // 焦点回到难度选择区域
    document.querySelector('.difficulty-buttons').scrollIntoView({ behavior: 'smooth' });
  });
  
  // 设置模态窗口按钮
  document.getElementById('save-settings-btn').addEventListener('click', saveSettings);
  document.getElementById('close-settings-btn').addEventListener('click', closeModal);
  
  // 音量滑块
  document.getElementById('setting-music-volume').addEventListener('input', (e) => {
    document.getElementById('music-volume-value').textContent = `${e.target.value}%`;
  });
  
  document.getElementById('setting-sound-volume').addEventListener('input', (e) => {
    document.getElementById('sound-volume-value').textContent = `${e.target.value}%`;
  });
  
  // 窗口调整大小
  window.addEventListener('resize', resizeGameBoard);
  
  // 键盘快捷键
  document.addEventListener('keydown', handleKeyPress);
  
  // 点击外部关闭模态窗口
  document.querySelectorAll('.modal').forEach(modal => {
    modal.addEventListener('click', (e) => {
      if (e.target === modal) {
        closeModal();
      }
    });
  });
}

// 更新设置表单
function updateSettingsForm() {
  document.getElementById('setting-sound').checked = gameState.settings.sound;
  document.getElementById('setting-music-volume').value = gameState.settings.musicVolume;
  document.getElementById('music-volume-value').textContent = `${gameState.settings.musicVolume}%`;
  document.getElementById('setting-sound-volume').value = gameState.settings.soundVolume;
  document.getElementById('sound-volume-value').textContent = `${gameState.settings.soundVolume}%`;
  document.getElementById('setting-show-grid').checked = gameState.settings.showGrid;
  document.getElementById('setting-auto-hint').checked = gameState.settings.autoHint;
  document.getElementById('setting-hint-count').value = gameState.settings.hintCount;
}

// 保存设置
function saveSettings() {
  gameState.settings.sound = document.getElementById('setting-sound').checked;
  gameState.settings.musicVolume = parseInt(document.getElementById('setting-music-volume').value);
  gameState.settings.soundVolume = parseInt(document.getElementById('setting-sound-volume').value);
  gameState.settings.showGrid = document.getElementById('setting-show-grid').checked;
  gameState.settings.autoHint = document.getElementById('setting-auto-hint').checked;
  gameState.settings.hintCount = parseInt(document.getElementById('setting-hint-count').value);
  
  applySettings();
  saveGameData();
  closeModal();
  showToast('设置已保存', 'success');
}

// 打开模态窗口
function openModal(modalName) {
  closeModal();
  if (modals[modalName]) {
    modals[modalName].classList.add('active');
  }
}

// 关闭模态窗口
function closeModal() {
  Object.values(modals).forEach(modal => {
    modal.classList.remove('active');
  });
}

// 调整游戏棋盘大小
function resizeGameBoard() {
  const containerWidth = gameBoard.parentElement.clientWidth;
  const containerHeight = gameBoard.parentElement.clientHeight;
  
  // 计算合适的卡牌大小
  const maxCardSizeByWidth = (containerWidth - (gameState.boardSize.cols + 1) * gameState.cardGap) / gameState.boardSize.cols;
  const maxCardSizeByHeight = (containerHeight - (gameState.boardSize.rows + 1) * gameState.cardGap) / gameState.boardSize.rows;
  
  const newCardSize = Math.min(maxCardSizeByWidth, maxCardSizeByHeight, 80); // 最大80px
  
  if (newCardSize !== gameState.cardSize) {
    gameState.cardSize = newCardSize;
    
    // 更新棋盘样式
    gameBoard.style.gridTemplateColumns = `repeat(${gameState.boardSize.cols}, ${newCardSize}px)`;
    gameBoard.style.gridTemplateRows = `repeat(${gameState.boardSize.rows}, ${newCardSize}px)`;
    
    // 更新连接线SVG大小
    connectionLinesElement.style.width = `${containerWidth}px`;
    connectionLinesElement.style.height = `${containerHeight}px`;
    
    // 重新渲染卡牌
    renderCards();
  }
}

// 改变难度
function changeDifficulty(difficulty) {
  // 更新难度按钮样式
  document.querySelectorAll('.difficulty-buttons .btn').forEach(btn => {
    btn.classList.remove('btn-primary');
    btn.classList.add('btn');
  });
  
  const selectedBtn = document.querySelector(`.difficulty-buttons .btn[data-difficulty="${difficulty}"]`);
  if (selectedBtn) {
    selectedBtn.classList.remove('btn');
    selectedBtn.classList.add('btn-primary');
  }
  
  // 更新游戏状态
  gameState.difficulty = difficulty;
  
  // 根据难度设置棋盘大小
  switch (difficulty) {
    case 'easy':
      gameState.boardSize.rows = 4;
      gameState.boardSize.cols = 4;
      break;
    case 'medium':
      gameState.boardSize.rows = 6;
      gameState.boardSize.cols = 6;
      break;
    case 'hard':
      gameState.boardSize.rows = 8;
      gameState.boardSize.cols = 8;
      break;
  }
  
  resizeGameBoard();
}

// 创建新游戏
function createNewGame() {
  // 重置游戏状态
  gameState.cards = [];
  gameState.selectedCard = null;
  gameState.disabledCards = false;
  gameState.moves = 0;
  gameState.gameStarted = false;
  gameState.history = [];
  gameState.hintsLeft = gameState.settings.hintCount;
  gameState.totalHintsUsed = 0;
  
  // 清除计时器
  if (gameState.timerInterval) {
    clearInterval(gameState.timerInterval);
    gameState.timerInterval = null;
  }
  
  gameState.timer = 0;
  
  // 更新显示
  updateGameStatus();
  
  // 清除棋盘
  gameBoard.innerHTML = '';
  
  // 清除连接线
  connectionLinesElement.innerHTML = '';
  
  // 生成卡牌
  generateCards();
  
  // 渲染卡牌
  renderCards();
  
  // 更新统计
  gameState.statistics.totalGames++;
  saveGameData();
  updateStatisticsDisplay();
}

// 生成卡牌
function generateCards() {
  const totalCards = gameState.boardSize.rows * gameState.boardSize.cols;
  const pairsNeeded = totalCards / 2;
  
  // 选择需要的图标
  const selectedIcons = gameState.cardIcons.slice(0, pairsNeeded);
  
  // 创建成对的卡牌
  const cardPairs = [];
  
  for (let i = 0; i < pairsNeeded; i++) {
    const icon = selectedIcons[i];
    
    // 创建一对卡牌
    for (let j = 0; j < 2; j++) {
      cardPairs.push({
        id: `${i}-${j}`,
        icon: icon,
        matched: false,
        row: -1,
        col: -1
      });
    }
  }
  
  // 洗牌
  shuffleArray(cardPairs);
  
  // 分配行列位置
  let index = 0;
  for (let row = 0; row < gameState.boardSize.rows; row++) {
    for (let col = 0; col < gameState.boardSize.cols; col++) {
      if (index < cardPairs.length) {
        cardPairs[index].row = row;
        cardPairs[index].col = col;
        index++;
      }
    }
  }
  
  gameState.cards = cardPairs;
}

// 渲染卡牌
function renderCards() {
  gameBoard.innerHTML = '';
  gameBoard.style.gridTemplateColumns = `repeat(${gameState.boardSize.cols}, ${gameState.cardSize}px)`;
  gameBoard.style.gridTemplateRows = `repeat(${gameState.boardSize.rows}, ${gameState.cardSize}px)`;
  
  gameState.cards.forEach(card => {
    if (!card.matched) {
      const cardElement = document.createElement('div');
      cardElement.className = 'card';
      cardElement.dataset.row = card.row;
      cardElement.dataset.col = card.col;
      cardElement.dataset.id = card.id;
      
      // 设置卡牌大小
      cardElement.style.width = `${gameState.cardSize}px`;
      cardElement.style.height = `${gameState.cardSize}px`;
      
      // 添加图标
      const iconElement = document.createElement('div');
      iconElement.className = 'card-icon';
      iconElement.textContent = card.icon;
      cardElement.appendChild(iconElement);
      
      // 添加点击事件
      cardElement.addEventListener('click', () => handleCardClick(card));
      
      gameBoard.appendChild(cardElement);
    }
  });
}

// 处理卡牌点击
function handleCardClick(card) {
  // 如果游戏未开始,开始游戏
  if (!gameState.gameStarted) {
    startGame();
  }
  
  // 如果卡牌已匹配或被禁用,不响应点击
  if (card.matched || gameState.disabledCards) {
    return;
  }
  
  // 如果点击的是已经选中的卡牌,不做任何操作
  if (gameState.selectedCard && gameState.selectedCard.id === card.id) {
    return;
  }
  
  // 记录移动历史
  if (gameState.selectedCard) {
    gameState.history.push({
      type: 'attempt',
      card1: { ...gameState.selectedCard },
      card2: { ...card }
    });
  }
  
  // 如果已经选择了一张卡牌,检查是否可以连接
  if (gameState.selectedCard) {
    const canConnect = canCardsConnect(gameState.selectedCard, card);
    
    // 禁用所有卡牌,直到动画完成
    gameState.disabledCards = true;
    
    // 高亮第二张卡牌
    highlightCard(card);
    
    // 如果可以连接,显示连接线
    if (canConnect) {
      // 显示连接线
      showConnection(gameState.selectedCard, card);
      
      // 延迟移除卡牌
      setTimeout(() => {
        // 标记为已匹配
        gameState.selectedCard.matched = true;
        card.matched = true;
        
        // 更新卡牌状态
        updateMatchedCards(gameState.selectedCard, card);
        
        // 清除选择
        gameState.selectedCard = null;
        gameState.disabledCards = false;
        
        // 增加移动次数
        gameState.moves++;
        
        // 更新游戏状态
        updateGameStatus();
        
        // 检查游戏是否结束
        checkGameOver();
      }, 500);
    } else {
      // 延迟清除选择
      setTimeout(() => {
        // 重置选中状态
        unhighlightCard(gameState.selectedCard);
        unhighlightCard(card);
        
        // 清除选择
        gameState.selectedCard = null;
        gameState.disabledCards = false;
        
        // 增加移动次数
        gameState.moves++;
        
        // 更新游戏状态
        updateGameStatus();
      }, 500);
    }
  } else {
    // 选择第一张卡牌
    gameState.selectedCard = card;
    highlightCard(card);
  }
}

// 高亮卡牌
function highlightCard(card) {
  const cardElement = document.querySelector(`.card[data-id="${card.id}"]`);
  if (cardElement) {
    cardElement.style.backgroundColor = 'var(--accent-tertiary)';
    cardElement.style.transform = 'scale(1.05)';
  }
}

// 取消高亮卡牌
function unhighlightCard(card) {
  const cardElement = document.querySelector(`.card[data-id="${card.id}"]`);
  if (cardElement) {
    cardElement.style.backgroundColor = '';
    cardElement.style.transform = '';
  }
}

// 更新已匹配卡牌
function updateMatchedCards(card1, card2) {
  const cardElements = [
    document.querySelector(`.card[data-id="${card1.id}"]`),
    document.querySelector(`.card[data-id="${card2.id}"]`)
  ];
  
  cardElements.forEach(element => {
    if (element) {
      element.classList.add('matched');
      
      // 动画结束后移除元素
      setTimeout(() => {
        if (element.parentNode) {
          element.parentNode.removeChild(element);
        }
      }, 500);
    }
  });
}

// 显示连接线
function showConnection(card1, card2) {
  // 清除之前的连接线
  connectionLinesElement.innerHTML = '';
  
  // 计算连接线路径
  const pathData = calculateConnectionPath(card1, card2);
  
  if (pathData) {
    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('d', pathData);
    path.classList.add('connection-path');
    connectionLinesElement.appendChild(path);
    
    // 一段时间后清除连接线
    setTimeout(() => {
      connectionLinesElement.innerHTML = '';
    }, 500);
  }
}

// 计算连接线路径
function calculateConnectionPath(card1, card2) {
  const cardSize = gameState.cardSize;
  const cardGap = gameState.cardGap;
  
  // 计算卡牌中心位置
  const center1 = {
    x: card1.col * (cardSize + cardGap) + cardSize / 2,
    y: card1.row * (cardSize + cardGap) + cardSize / 2
  };
  
  const center2 = {
    x: card2.col * (cardSize + cardGap) + cardSize / 2,
    y: card2.row * (cardSize + cardGap) + cardSize / 2
  };
  
  // 获取连接点
  const points = findConnectionPoints(card1, card2);
  
  if (points && points.length >= 2) {
    let pathData = `M ${points[0].x} ${points[0].y}`;
    
    // 添加中间点
    for (let i = 1; i < points.length; i++) {
      pathData += ` L ${points[i].x} ${points[i].y}`;
    }
    
    return pathData;
  }
  
  return null;
}

// 查找连接点
function findConnectionPoints(card1, card2) {
  // 这里是简化的实现,实际上应该根据连连看规则计算有效的连接点
  // 真实实现需要考虑0折、1折和2折连接
  
  // 简单返回卡牌中心点
  const cardSize = gameState.cardSize;
  const cardGap = gameState.cardGap;
  
  return [
    {
      x: card1.col * (cardSize + cardGap) + cardSize / 2,
      y: card1.row * (cardSize + cardGap) + cardSize / 2
    },
    {
      x: card2.col * (cardSize + cardGap) + cardSize / 2,
      y: card2.row * (cardSize + cardGap) + cardSize / 2
    }
  ];
}

// 检查两张卡牌是否可以连接
function canCardsConnect(card1, card2) {
  // 确保图标相同
  if (card1.icon !== card2.icon) {
    return false;
  }
  
  // 检查是否是同一张卡牌
  if (card1.id === card2.id) {
    return false;
  }
  
  // 检查0折连接(直线连接)
  if (canConnectDirectly(card1, card2)) {
    return true;
  }
  
  // 检查1折连接
  if (canConnectWithOneCorner(card1, card2)) {
    return true;
  }
  
  // 检查2折连接
  if (canConnectWithTwoCorners(card1, card2)) {
    return true;
  }
  
  return false;
}

// 检查是否可以直接连接
function canConnectDirectly(card1, card2) {
  // 如果不在同一行或同一列,不能直接连接
  if (card1.row !== card2.row && card1.col !== card2.col) {
    return false;
  }
  
  // 同一行
  if (card1.row === card2.row) {
    const minCol = Math.min(card1.col, card2.col);
    const maxCol = Math.max(card1.col, card2.col);
    
    // 检查中间是否有卡牌
    for (let col = minCol + 1; col < maxCol; col++) {
      const blockingCard = findCardAtPosition(card1.row, col);
      if (blockingCard && !blockingCard.matched) {
        return false;
      }
    }
    
    return true;
  }
  
  // 同一列
  if (card1.col === card2.col) {
    const minRow = Math.min(card1.row, card2.row);
    const maxRow = Math.max(card1.row, card2.row);
    
    // 检查中间是否有卡牌
    for (let row = minRow + 1; row < maxRow; row++) {
      const blockingCard = findCardAtPosition(row, card1.col);
      if (blockingCard && !blockingCard.matched) {
        return false;
      }
    }
    
    return true;
  }
  
  return false;
}

// 检查是否可以通过一个拐角连接
function canConnectWithOneCorner(card1, card2) {
  // 检查两个可能的拐角点
  const corner1 = { row: card1.row, col: card2.col };
  const corner2 = { row: card2.row, col: card1.col };
  
  // 检查拐角1
  if (isCornerClear(corner1)) {
    if (canConnectDirectly(card1, corner1) && canConnectDirectly(corner1, card2)) {
      return true;
    }
  }
  
  // 检查拐角2
  if (isCornerClear(corner2)) {
    if (canConnectDirectly(card1, corner2) && canConnectDirectly(corner2, card2)) {
      return true;
    }
  }
  
  return false;
}

// 检查是否可以通过两个拐角连接
function canConnectWithTwoCorners(card1, card2) {
  // 尝试所有可能的水平-垂直路径
  // 先水平移动再垂直移动
  for (let row = 0; row < gameState.boardSize.rows; row++) {
    const corner1 = { row: card1.row, col: card1.col };
    const corner2 = { row: row, col: card1.col };
    const corner3 = { row: row, col: card2.col };
    const corner4 = { row: card2.row, col: card2.col };
    
    if (isCornerClear(corner2) && isCornerClear(corner3)) {
      if (canConnectDirectly(corner1, corner2) && 
          canConnectDirectly(corner2, corner3) && 
          canConnectDirectly(corner3, corner4)) {
        return true;
      }
    }
  }
  
  // 先垂直移动再水平移动
  for (let col = 0; col < gameState.boardSize.cols; col++) {
    const corner1 = { row: card1.row, col: card1.col };
    const corner2 = { row: card1.row, col: col };
    const corner3 = { row: card2.row, col: col };
    const corner4 = { row: card2.row, col: card2.col };
    
    if (isCornerClear(corner2) && isCornerClear(corner3)) {
      if (canConnectDirectly(corner1, corner2) && 
          canConnectDirectly(corner2, corner3) && 
          canConnectDirectly(corner3, corner4)) {
        return true;
      }
    }
  }
  
  return false;
}

// 检查拐角点是否为空
function isCornerClear(position) {
  // 检查是否在棋盘内
  if (position.row < 0 || position.row >= gameState.boardSize.rows || 
      position.col < 0 || position.col >= gameState.boardSize.cols) {
    return false;
  }
  
  // 检查该位置是否有未匹配的卡牌
  const card = findCardAtPosition(position.row, position.col);
  return !card || card.matched;
}

// 查找指定位置的卡牌
function findCardAtPosition(row, col) {
  return gameState.cards.find(card => 
    card.row === row && 
    card.col === col && 
    !card.matched
  );
}

// 开始游戏
function startGame() {
  gameState.gameStarted = true;
  
  // 启动计时器
  gameState.timerInterval = setInterval(() => {
    gameState.timer++;
    updateGameStatus();
  }, 1000);
}

// 停止游戏
function stopGame() {
  if (gameState.timerInterval) {
    clearInterval(gameState.timerInterval);
    gameState.timerInterval = null;
  }
}

// 检查游戏是否结束
function checkGameOver() {
  // 检查是否所有卡牌都已匹配
  const allMatched = gameState.cards.every(card => card.matched);
  
  if (allMatched) {
    // 停止游戏
    stopGame();
    
    // 更新统计数据
    gameState.statistics.wins++;
    
    // 更新最佳时间
    if (!gameState.statistics.bestTime || gameState.timer < gameState.statistics.bestTime) {
      gameState.statistics.bestTime = gameState.timer;
    }
    
    saveGameData();
    updateStatisticsDisplay();
    
    // 显示游戏结束模态窗口
    setTimeout(() => {
      showGameOverModal();
    }, 500);
  }
}

// 显示游戏结束模态窗口
function showGameOverModal() {
  // 更新模态窗口内容
  document.getElementById('final-time').textContent = formatTime(gameState.timer);
  document.getElementById('final-moves').textContent = gameState.moves;
  document.getElementById('final-hints').textContent = gameState.totalHintsUsed;
  
  // 打开模态窗口
  openModal('gameOver');
  
  // 显示胜利消息
  showToast('恭喜你,游戏胜利!', 'success');
}

// 显示提示
function showHint() {
  if (gameState.hintsLeft <= 0) {
    showToast('没有提示次数了!', 'warning');
    return;
  }
  
  // 找到一对可连接的卡牌
  const hintPair = findHintPair();
  
  if (hintPair) {
    // 高亮提示的卡牌
    const cardElements = [
      document.querySelector(`.card[data-id="${hintPair.card1.id}"]`),
      document.querySelector(`.card[data-id="${hintPair.card2.id}"]`)
    ];
    
    cardElements.forEach(element => {
      if (element) {
        element.classList.add('hint');
        
        // 3秒后移除高亮
        setTimeout(() => {
          element.classList.remove('hint');
        }, 3000);
      }
    });
    
    // 减少提示次数
    gameState.hintsLeft--;
    gameState.totalHintsUsed++;
    updateGameStatus();
    
    showToast('找到一对可连接的卡牌!', 'info');
  } else {
    showToast('当前没有可连接的卡牌,请尝试洗牌!', 'warning');
  }
}

// 寻找可提示的卡牌对
function findHintPair() {
  const unmatchedCards = gameState.cards.filter(card => !card.matched);
  
  // 尝试找到一对可连接的卡牌
  for (let i = 0; i < unmatchedCards.length; i++) {
    for (let j = i + 1; j < unmatchedCards.length; j++) {
      if (unmatchedCards[i].icon === unmatchedCards[j].icon && 
          canCardsConnect(unmatchedCards[i], unmatchedCards[j])) {
        return {
          card1: unmatchedCards[i],
          card2: unmatchedCards[j]
        };
      }
    }
  }
  
  return null;
}

// 洗牌
function shuffleCards() {
  // 收集所有未匹配的卡牌
  const unmatchedCards = gameState.cards.filter(card => !card.matched);
  
  // 洗牌
  shuffleArray(unmatchedCards);
  
  // 重新分配位置
  let index = 0;
  for (let row = 0; row < gameState.boardSize.rows; row++) {
    for (let col = 0; col < gameState.boardSize.cols; col++) {
      // 检查该位置是否有已匹配的卡牌
      const matchedCard = gameState.cards.find(c => 
        c.row === row && c.col === col && c.matched
      );
      
      // 如果没有已匹配的卡牌,分配新卡牌
      if (!matchedCard && index < unmatchedCards.length) {
        unmatchedCards[index].row = row;
        unmatchedCards[index].col = col;
        index++;
      }
    }
  }
  
  // 重置选中状态
  if (gameState.selectedCard) {
    unhighlightCard(gameState.selectedCard);
    gameState.selectedCard = null;
  }
  
  // 清除连接线
  connectionLinesElement.innerHTML = '';
  
  // 重新渲染卡牌
  renderCards();
  
  showToast('卡牌已重新洗牌!', 'info');
}

// 撤销上一步
function undoLastMove() {
  if (gameState.history.length === 0) {
    showToast('没有可撤销的操作!', 'warning');
    return;
  }
  
  // 获取最后一次操作
  const lastAction = gameState.history.pop();
  
  // 撤销移动
  if (lastAction.type === 'attempt') {
    // 重置卡牌状态
    if (lastAction.card1 && lastAction.card2) {
      lastAction.card1.matched = false;
      lastAction.card2.matched = false;
    }
    
    // 减少移动次数
    gameState.moves = Math.max(0, gameState.moves - 1);
    
    // 更新游戏状态
    updateGameStatus();
    
    // 重新渲染卡牌
    renderCards();
    
    showToast('已撤销上一步操作!', 'info');
  }
}

// 更新游戏状态显示
function updateGameStatus() {
  // 更新剩余卡牌
  const remainingCards = gameState.cards.filter(card => !card.matched).length;
  cardsLeftElement.textContent = remainingCards;
  
  // 更新计时器
  timerElement.textContent = formatTime(gameState.timer);
  
  // 更新移动次数
  movesElement.textContent = gameState.moves;
  
  // 更新剩余提示次数
  hintsLeftElement.textContent = gameState.hintsLeft;
}

// 更新统计显示
function updateStatisticsDisplay() {
  totalGamesElement.textContent = gameState.statistics.totalGames;
  winsCountElement.textContent = gameState.statistics.wins;
  
  // 计算胜率
  const winRate = gameState.statistics.totalGames > 0 
    ? Math.round((gameState.statistics.wins / gameState.statistics.totalGames) * 100)
    : 0;
  winRateElement.textContent = `${winRate}%`;
  
  // 更新最佳时间
  bestTimeElement.textContent = gameState.statistics.bestTime 
    ? formatTime(gameState.statistics.bestTime)
    : '--:--';
}

// 格式化时间
function formatTime(seconds) {
  const minutes = Math.floor(seconds / 60);
  const remainingSeconds = seconds % 60;
  
  return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}

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

// 显示消息提示
function showToast(message, type = 'info') {
  // 确保toast元素存在
  if (!toastElement) {
    return;
  }
  
  // 设置消息内容
  toastElement.textContent = message;
  
  // 设置消息类型
  toastElement.className = 'toast';
  if (type) {
    toastElement.classList.add(type);
  }
  
  // 显示toast
  toastElement.classList.add('active');
  
  // 3秒后隐藏
  setTimeout(() => {
    toastElement.classList.remove('active');
  }, 3000);
}

// 处理键盘快捷键
function handleKeyPress(event) {
  // 如果有模态窗口打开,只处理关闭操作
  const modalOpen = Object.values(modals).some(modal => modal.classList.contains('active'));
  
  if (modalOpen) {
    if (event.key === 'Escape') {
      closeModal();
    }
    return;
  }
  
  // 游戏快捷键
  switch (event.key) {
    case 'F2':
      event.preventDefault();
      createNewGame();
      break;
    case 'r':
    case 'R':
      if (event.ctrlKey) {
        event.preventDefault();
        createNewGame();
      }
      break;
    case 'h':
    case 'H':
      showHint();
      break;
    case 's':
    case 'S':
      shuffleCards();
      break;
    case 'z':
    case 'Z':
      if (event.ctrlKey) {
        event.preventDefault();
        undoLastMove();
      }
      break;
    case '1':
      changeDifficulty('easy');
      createNewGame();
      break;
    case '2':
      changeDifficulty('medium');
      createNewGame();
      break;
    case '3':
      changeDifficulty('hard');
      createNewGame();
      break;
  }
}

// 当页面加载完成时初始化游戏
document.addEventListener('DOMContentLoaded', initGame);

界面设计

  • 使用CSS Grid布局创建游戏棋盘
  • 为游戏方块添加悬停和点击效果
  • 实现连线动画,直观显示匹配路径
  • 添加游戏状态栏,显示时间、分数和剩余方块数量
  • 设计游戏控制面板,包括开始、重置、提示等功能按钮

如何运行

  1. 确保安装了Node.js和npm
  2. 进入项目目录
  3. 安装依赖:npm install
  4. 启动应用:npm start

学习要点

  • 理解路径查找算法的实现(连连看核心)
  • 学习游戏状态管理和逻辑控制
  • 掌握DOM操作和动态元素管理
  • 了解游戏动画效果的实现
  • 学习如何在Electron应用中实现复杂的游戏算法

鸿蒙PC适配改造指南

1. 环境准备

  • 系统要求:Windows 10/11、8GB RAM以上、20GB可用空间

  • 工具安装
    DevEco Studio 5.0+(安装鸿蒙SDK API 20+)

  • Node.js 18.x+

2. 获取Electron鸿蒙编译产物

  1. 登录Electron 鸿蒙官方仓库

  2. 下载Electron 34+版本的Release包(.zip格式)

  3. 解压到项目目录,确认electron/libs/arm64-v8a/下包含核心.so库

3. 部署应用代码

将Electron应用代码按以下目录结构放置:
在这里插入图片描述


web_engine/src/main/resources/resfile/resources/app/
├── main.js
├── package.json
└── src/
    ├── index.html
    ├── preload.js
    ├── renderer.js
    └── style.css

4. 配置与运行

  1. 打开项目:在DevEco Studio中打开ohos_hap目录

  2. 配置签名
    进入File → Project Structure → Signing Configs

  3. 自动生成调试签名或导入已有签名

  4. 连接设备
    启用鸿蒙设备开发者模式和USB调试

  5. 通过USB Type-C连接电脑

  6. 编译运行:点击Run按钮或按Shift+F10

5. 验证检查项

  • ✅ 应用窗口正常显示

  • ✅ 窗口大小可调整,响应式布局生效

  • ✅ 控制台无"SysCap不匹配"或"找不到.so文件"错误

  • ✅ 动画效果正常播放

跨平台兼容性

平台 适配策略 特殊处理
Windows 标准Electron运行 无特殊配置
macOS 标准Electron运行 保留dock图标激活逻辑
Linux 标准Electron运行 确保系统依赖库完整
鸿蒙PC 通过Electron鸿蒙适配层 禁用硬件加速,使用特定目录结构

鸿蒙开发调试技巧

1. 日志查看

在DevEco Studio的Log面板中过滤"Electron"关键词,查看应用运行日志和错误信息。

2. 常见问题解决

  • "SysCap不匹配"错误:检查module.json5中的reqSysCapabilities,只保留必要系统能力

  • "找不到.so文件"错误:确认arm64-v8a目录下四个核心库文件完整

  • 窗口不显示:在main.js中添加app.disableHardwareAcceleration()

  • 动画卡顿:简化CSS动画效果,减少重绘频率

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐