Electron for 鸿蒙PC项目实战案例 - 连连看小游戏
这是一个基于Electron for 鸿蒙PC的连连看游戏实现,专为初学者设计。连连看是一款经典的配对消除游戏,玩家需要找出相同的两个图案,并通过最多三条直线连接它们进行消除。游戏锻炼玩家的观察力和反应速度。实现细节游戏核心逻辑棋盘初始化:随机生成游戏图案,确保每种图案都有且仅有两个路径检测算法:直线检测:检查两个方块是否在同一行或同一列,且中间无阻挡一弯检测:通过一个转折点连接两个方块两弯检测:
连连看 - Electron for 鸿蒙PC项目实战案例
项目概述
这是一个基于Electron for 鸿蒙PC的连连看游戏实现,专为初学者设计。连连看是一款经典的配对消除游戏,玩家需要找出相同的两个图案,并通过最多三条直线连接它们进行消除。游戏锻炼玩家的观察力和反应速度。
技术要点
核心技术
- 路径查找算法:实现连连看的路径搜索算法,支持0弯、1弯、2弯和3弯路径检测
- 图像渲染:加载和显示游戏图案,实现方块的消除动画
- 游戏逻辑控制:管理游戏流程、计分系统和难度设置
- DOM操作:动态创建和管理游戏元素
主要功能
- 图案匹配:找出相同的图案并进行消除
- 连线显示:直观显示匹配图案之间的连接路径
- 倒计时系统:设置游戏时间限制,增加挑战性
- 分数系统:根据消除速度和难度计算分数
- 难度调节:提供不同的游戏难度级别
- 提示功能:在玩家困难时提供匹配提示
- 重新洗牌:当无法找到匹配时,可以重新排列图案
Electron特性应用
- 使用
Menu模块创建自定义应用菜单,提供游戏控制选项 - 使用
ipcMain和ipcRenderer进行进程间通信 - 使用
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布局创建游戏棋盘
- 为游戏方块添加悬停和点击效果
- 实现连线动画,直观显示匹配路径
- 添加游戏状态栏,显示时间、分数和剩余方块数量
- 设计游戏控制面板,包括开始、重置、提示等功能按钮
如何运行
- 确保安装了Node.js和npm
- 进入项目目录
- 安装依赖:
npm install - 启动应用:
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鸿蒙编译产物
-
下载Electron 34+版本的Release包(.zip格式)
-
解压到项目目录,确认
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. 配置与运行
-
打开项目:在DevEco Studio中打开ohos_hap目录
-
配置签名:
进入File → Project Structure → Signing Configs -
自动生成调试签名或导入已有签名
-
连接设备:
启用鸿蒙设备开发者模式和USB调试 -
通过USB Type-C连接电脑
-
编译运行:点击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动画效果,减少重绘频率
更多推荐




所有评论(0)