feat: 添加贪吃蛇和记忆配对纸牌游戏
- 贪吃蛇游戏: Canvas渲染、方向控制、碰撞检测、计分系统 - 记忆配对纸牌: 3D翻转动画、配对消除、计时器、胜利判定
This commit is contained in:
@@ -0,0 +1,209 @@
|
|||||||
|
class SnakeGame {
|
||||||
|
constructor(canvasId, scoreId) {
|
||||||
|
this.canvas = document.getElementById(canvasId);
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.scoreElement = document.getElementById(scoreId);
|
||||||
|
|
||||||
|
this.gridSize = 20;
|
||||||
|
this.tileCount = this.canvas.width / this.gridSize;
|
||||||
|
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
window.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.snake = [
|
||||||
|
{ x: 10, y: 10 },
|
||||||
|
{ x: 10, y: 11 },
|
||||||
|
{ x: 10, y: 12 }
|
||||||
|
];
|
||||||
|
this.food = this.getRandomFood();
|
||||||
|
this.dx = 0;
|
||||||
|
this.dy = -1;
|
||||||
|
this.score = 0;
|
||||||
|
this.updateScore();
|
||||||
|
this.gameOver = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomFood() {
|
||||||
|
let newFood;
|
||||||
|
while (true) {
|
||||||
|
newFood = {
|
||||||
|
x: Math.floor(Math.random() * this.tileCount),
|
||||||
|
y: Math.floor(Math.random() * this.tileCount)
|
||||||
|
};
|
||||||
|
// 确保食物不生成在蛇身上
|
||||||
|
let collision = this.snake.some(segment => segment.x === newFood.x && segment.y === newFood.y);
|
||||||
|
if (!collision) break;
|
||||||
|
}
|
||||||
|
return newFood;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(e) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowUp':
|
||||||
|
if (this.dy === 0) { this.dx = 0; this.dy = -1; }
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
if (this.dy === 0) { this.dx = 0; this.dy = 1; }
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
if (this.dx === 0) { this.dx = -1; this.dy = 0; }
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
if (this.dx === 0) { this.dx = 1; this.dy = 0; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.gameOver) return;
|
||||||
|
|
||||||
|
const head = { x: this.snake[0].x + this.dx, y: this.snake[0].y + this.dy };
|
||||||
|
|
||||||
|
// 碰撞墙壁检测
|
||||||
|
if (head.x < 0 || head.x >= this.tileCount || head.y < 0 || head.y >= this.tileCount) {
|
||||||
|
this.endGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 碰撞自身检测
|
||||||
|
if (this.snake.some(segment => segment.x === head.x && segment.y === head.y)) {
|
||||||
|
this.endGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.snake.unshift(head);
|
||||||
|
|
||||||
|
// 进食检测
|
||||||
|
if (head.x === this.food.x && head.y === this.food.y) {
|
||||||
|
this.score += 10;
|
||||||
|
this.updateScore();
|
||||||
|
this.food = this.getRandomFood();
|
||||||
|
} else {
|
||||||
|
this.snake.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
// 清屏
|
||||||
|
this.ctx.fillStyle = '#1a1a1a';
|
||||||
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
// 画网格背景 (可选)
|
||||||
|
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
for (let i = 0; i <= this.tileCount; i++) {
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(i * this.gridSize, 0);
|
||||||
|
this.ctx.lineTo(i * this.gridSize, this.canvas.height);
|
||||||
|
this.ctx.stroke();
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(0, i * this.gridSize);
|
||||||
|
this.ctx.lineTo(this.canvas.width, i * this.gridSize);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 画食物
|
||||||
|
const foodX = this.food.x * this.gridSize + this.gridSize / 2;
|
||||||
|
const foodY = this.food.y * this.gridSize + this.gridSize / 2;
|
||||||
|
const foodRadius = this.gridSize / 2 - 2;
|
||||||
|
|
||||||
|
const gradient = this.ctx.createRadialGradient(foodX, foodY, 2, foodX, foodY, foodRadius);
|
||||||
|
gradient.addColorStop(0, '#ff7675');
|
||||||
|
gradient.addColorStop(1, '#d63031');
|
||||||
|
|
||||||
|
this.ctx.fillStyle = gradient;
|
||||||
|
this.ctx.shadowBlur = 15;
|
||||||
|
this.ctx.shadowColor = '#d63031';
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(foodX, foodY, foodRadius, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
this.ctx.shadowBlur = 0;
|
||||||
|
|
||||||
|
// 画蛇
|
||||||
|
this.snake.forEach((segment, index) => {
|
||||||
|
const isHead = index === 0;
|
||||||
|
const x = segment.x * this.gridSize;
|
||||||
|
const y = segment.y * this.gridSize;
|
||||||
|
const size = this.gridSize - 2;
|
||||||
|
const radius = isHead ? 8 : 4;
|
||||||
|
|
||||||
|
this.ctx.fillStyle = isHead ? '#55efc4' : '#00b894';
|
||||||
|
|
||||||
|
// 蛇头阴影
|
||||||
|
if (isHead) {
|
||||||
|
this.ctx.shadowBlur = 10;
|
||||||
|
this.ctx.shadowColor = '#55efc4';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.roundRect(x + 1, y + 1, size, size, radius);
|
||||||
|
this.ctx.fill();
|
||||||
|
this.ctx.shadowBlur = 0;
|
||||||
|
|
||||||
|
// 眼睛
|
||||||
|
if (isHead) {
|
||||||
|
this.ctx.fillStyle = '#2d3436';
|
||||||
|
const eyeSize = 3;
|
||||||
|
const offset = 5;
|
||||||
|
|
||||||
|
// 根据方向画眼睛
|
||||||
|
if (this.dy === -1) { // Up
|
||||||
|
this.ctx.fillRect(x + offset, y + offset, eyeSize, eyeSize);
|
||||||
|
this.ctx.fillRect(x + size - offset - eyeSize, y + offset, eyeSize, eyeSize);
|
||||||
|
} else if (this.dy === 1) { // Down
|
||||||
|
this.ctx.fillRect(x + offset, y + size - offset - eyeSize, eyeSize, eyeSize);
|
||||||
|
this.ctx.fillRect(x + size - offset - eyeSize, y + size - offset - eyeSize, eyeSize, eyeSize);
|
||||||
|
} else if (this.dx === -1) { // Left
|
||||||
|
this.ctx.fillRect(x + offset, y + offset, eyeSize, eyeSize);
|
||||||
|
this.ctx.fillRect(x + offset, y + size - offset - eyeSize, eyeSize, eyeSize);
|
||||||
|
} else { // Right
|
||||||
|
this.ctx.fillRect(x + size - offset - eyeSize, y + offset, eyeSize, eyeSize);
|
||||||
|
this.ctx.fillRect(x + size - offset - eyeSize, y + size - offset - eyeSize, eyeSize, eyeSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.gameOver) {
|
||||||
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.75)';
|
||||||
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
this.ctx.shadowBlur = 20;
|
||||||
|
this.ctx.shadowColor = 'rgba(255, 255, 255, 0.5)';
|
||||||
|
this.ctx.fillStyle = 'white';
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
this.ctx.font = 'bold 40px Segoe UI';
|
||||||
|
this.ctx.fillText('GAME OVER', this.canvas.width / 2, this.canvas.height / 2);
|
||||||
|
|
||||||
|
this.ctx.font = '20px Segoe UI';
|
||||||
|
this.ctx.fillStyle = '#bdc3c7';
|
||||||
|
this.ctx.fillText('按空格重新开始', this.canvas.width / 2, this.canvas.height / 2 + 50);
|
||||||
|
this.ctx.shadowBlur = 0;
|
||||||
|
this.ctx.textAlign = 'start';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScore() {
|
||||||
|
this.scoreElement.innerText = this.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
endGame() {
|
||||||
|
this.gameOver = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
this.update();
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = new SnakeGame('snakeGame', 'score');
|
||||||
|
setInterval(() => game.run(), 100);
|
||||||
|
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (e.code === 'Space' && game.gameOver) {
|
||||||
|
game.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>贪吃蛇大作战</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background: radial-gradient(circle, #2c3e50, #000);
|
||||||
|
color: white;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#game-container {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5), inset 0 0 10px rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#score-board {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
#score {
|
||||||
|
color: #2ecc71;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="score-board">分数: <span id="score">0</span></div>
|
||||||
|
<div id="game-container">
|
||||||
|
<canvas id="snakeGame" width="400" height="400"></canvas>
|
||||||
|
</div>
|
||||||
|
<script src="game.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>记忆配对纸牌</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-shadow: 0 0 20px rgba(0, 255, 255, 0.5);
|
||||||
|
background: linear-gradient(90deg, #00ffff, #ff00ff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
#stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 10px 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
color: #00ffcc;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
#game-board {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 100px);
|
||||||
|
gap: 15px;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
width: 100px;
|
||||||
|
height: 140px;
|
||||||
|
position: relative;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: transform 0.6s ease, opacity 0.5s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.card.flipped {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
.card.matched {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: rotateY(180deg) scale(0.8);
|
||||||
|
}
|
||||||
|
.card-face {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3rem;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.card-back {
|
||||||
|
background: linear-gradient(145deg, #e94560, #c23a51);
|
||||||
|
border: 3px solid #ff6b6b;
|
||||||
|
}
|
||||||
|
.card-back::before {
|
||||||
|
content: '?';
|
||||||
|
font-size: 3rem;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.card-front {
|
||||||
|
background: linear-gradient(145deg, #f8f9fa, #e9ecef);
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
border: 3px solid #ddd;
|
||||||
|
}
|
||||||
|
.card:hover:not(.flipped):not(.matched) {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
#restart-btn {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
background: linear-gradient(90deg, #00b894, #00cec9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 206, 201, 0.4);
|
||||||
|
}
|
||||||
|
#restart-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 206, 201, 0.6);
|
||||||
|
}
|
||||||
|
#win-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: linear-gradient(145deg, #2d3436, #1e272e);
|
||||||
|
padding: 50px;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 0 50px rgba(0, 255, 255, 0.3);
|
||||||
|
border: 2px solid rgba(0, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
.modal-content h2 {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: linear-gradient(90deg, #f1c40f, #e74c3c);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
.modal-content p {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>记忆配对纸牌</h1>
|
||||||
|
<div id="stats">
|
||||||
|
<div class="stat">步数: <span id="moves" class="stat-value">0</span></div>
|
||||||
|
<div class="stat">时间: <span id="timer" class="stat-value">0:00</span></div>
|
||||||
|
<div class="stat">配对: <span id="pairs" class="stat-value">0/8</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="game-board"></div>
|
||||||
|
<button id="restart-btn">重新开始</button>
|
||||||
|
|
||||||
|
<div id="win-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>🎉 恭喜胜利!</h2>
|
||||||
|
<p>你用了 <span id="final-moves">0</span> 步,耗时 <span id="final-time">0:00</span></p>
|
||||||
|
<button id="play-again-btn" style="padding: 15px 40px; font-size: 1.2rem; background: linear-gradient(90deg, #00b894, #00cec9); border: none; border-radius: 30px; color: white; cursor: pointer; font-weight: bold;">再来一局</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="memory.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
class MemoryGame {
|
||||||
|
constructor() {
|
||||||
|
this.symbols = ['🍎', '🍊', '🍋', '🍇', '🍓', '🍒', '🥝', '🍑'];
|
||||||
|
this.cards = [];
|
||||||
|
this.flippedCards = [];
|
||||||
|
this.matchedPairs = 0;
|
||||||
|
this.moves = 0;
|
||||||
|
this.timer = null;
|
||||||
|
this.seconds = 0;
|
||||||
|
this.isLocked = false;
|
||||||
|
this.gameStarted = false;
|
||||||
|
|
||||||
|
this.boardEl = document.getElementById('game-board');
|
||||||
|
this.movesEl = document.getElementById('moves');
|
||||||
|
this.timerEl = document.getElementById('timer');
|
||||||
|
this.pairsEl = document.getElementById('pairs');
|
||||||
|
this.winModal = document.getElementById('win-modal');
|
||||||
|
this.finalMovesEl = document.getElementById('final-moves');
|
||||||
|
this.finalTimeEl = document.getElementById('final-time');
|
||||||
|
|
||||||
|
document.getElementById('restart-btn').addEventListener('click', () => this.init());
|
||||||
|
document.getElementById('play-again-btn').addEventListener('click', () => {
|
||||||
|
this.winModal.style.display = 'none';
|
||||||
|
this.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 重置状态
|
||||||
|
this.cards = [];
|
||||||
|
this.flippedCards = [];
|
||||||
|
this.matchedPairs = 0;
|
||||||
|
this.moves = 0;
|
||||||
|
this.seconds = 0;
|
||||||
|
this.isLocked = false;
|
||||||
|
this.gameStarted = false;
|
||||||
|
|
||||||
|
// 清除计时器
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新UI
|
||||||
|
this.movesEl.textContent = '0';
|
||||||
|
this.timerEl.textContent = '0:00';
|
||||||
|
this.pairsEl.textContent = '0/8';
|
||||||
|
|
||||||
|
// 生成卡牌
|
||||||
|
this.generateCards();
|
||||||
|
this.renderBoard();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCards() {
|
||||||
|
// 创建配对卡牌
|
||||||
|
const cardPairs = [...this.symbols, ...this.symbols];
|
||||||
|
// 洗牌 (Fisher-Yates shuffle)
|
||||||
|
for (let i = cardPairs.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[cardPairs[i], cardPairs[j]] = [cardPairs[j], cardPairs[i]];
|
||||||
|
}
|
||||||
|
this.cards = cardPairs.map((symbol, index) => ({
|
||||||
|
id: index,
|
||||||
|
symbol: symbol,
|
||||||
|
isFlipped: false,
|
||||||
|
isMatched: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBoard() {
|
||||||
|
this.boardEl.innerHTML = '';
|
||||||
|
this.cards.forEach(card => {
|
||||||
|
const cardEl = document.createElement('div');
|
||||||
|
cardEl.className = 'card';
|
||||||
|
cardEl.dataset.id = card.id;
|
||||||
|
cardEl.innerHTML = `
|
||||||
|
<div class="card-face card-back"></div>
|
||||||
|
<div class="card-face card-front">${card.symbol}</div>
|
||||||
|
`;
|
||||||
|
cardEl.addEventListener('click', () => this.flipCard(card.id));
|
||||||
|
this.boardEl.appendChild(cardEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
flipCard(id) {
|
||||||
|
const card = this.cards[id];
|
||||||
|
const cardEl = this.boardEl.querySelector(`[data-id="${id}"]`);
|
||||||
|
|
||||||
|
// 防止重复点击或锁定状态
|
||||||
|
if (this.isLocked || card.isFlipped || card.isMatched) return;
|
||||||
|
|
||||||
|
// 开始计时
|
||||||
|
if (!this.gameStarted) {
|
||||||
|
this.gameStarted = true;
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻牌
|
||||||
|
card.isFlipped = true;
|
||||||
|
cardEl.classList.add('flipped');
|
||||||
|
this.flippedCards.push(card);
|
||||||
|
|
||||||
|
// 检查是否翻了两张
|
||||||
|
if (this.flippedCards.length === 2) {
|
||||||
|
this.moves++;
|
||||||
|
this.movesEl.textContent = this.moves;
|
||||||
|
this.checkMatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMatch() {
|
||||||
|
const [card1, card2] = this.flippedCards;
|
||||||
|
|
||||||
|
if (card1.symbol === card2.symbol) {
|
||||||
|
// 配对成功
|
||||||
|
this.matchSuccess(card1, card2);
|
||||||
|
} else {
|
||||||
|
// 配对失败
|
||||||
|
this.matchFail(card1, card2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchSuccess(card1, card2) {
|
||||||
|
card1.isMatched = true;
|
||||||
|
card2.isMatched = true;
|
||||||
|
this.matchedPairs++;
|
||||||
|
this.pairsEl.textContent = `${this.matchedPairs}/8`;
|
||||||
|
|
||||||
|
const cardEl1 = this.boardEl.querySelector(`[data-id="${card1.id}"]`);
|
||||||
|
const cardEl2 = this.boardEl.querySelector(`[data-id="${card2.id}"]`);
|
||||||
|
|
||||||
|
// 延迟消除动画
|
||||||
|
setTimeout(() => {
|
||||||
|
cardEl1.classList.add('matched');
|
||||||
|
cardEl2.classList.add('matched');
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
this.flippedCards = [];
|
||||||
|
|
||||||
|
// 检查胜利
|
||||||
|
if (this.matchedPairs === 8) {
|
||||||
|
this.win();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchFail(card1, card2) {
|
||||||
|
this.isLocked = true;
|
||||||
|
|
||||||
|
// 延迟翻回
|
||||||
|
setTimeout(() => {
|
||||||
|
card1.isFlipped = false;
|
||||||
|
card2.isFlipped = false;
|
||||||
|
|
||||||
|
const cardEl1 = this.boardEl.querySelector(`[data-id="${card1.id}"]`);
|
||||||
|
const cardEl2 = this.boardEl.querySelector(`[data-id="${card2.id}"]`);
|
||||||
|
cardEl1.classList.remove('flipped');
|
||||||
|
cardEl2.classList.remove('flipped');
|
||||||
|
|
||||||
|
this.flippedCards = [];
|
||||||
|
this.isLocked = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
startTimer() {
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.seconds++;
|
||||||
|
const mins = Math.floor(this.seconds / 60);
|
||||||
|
const secs = this.seconds % 60;
|
||||||
|
this.timerEl.textContent = `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
win() {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
|
||||||
|
const mins = Math.floor(this.seconds / 60);
|
||||||
|
const secs = this.seconds % 60;
|
||||||
|
const timeStr = `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
|
this.finalMovesEl.textContent = this.moves;
|
||||||
|
this.finalTimeEl.textContent = timeStr;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.winModal.style.display = 'flex';
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动游戏
|
||||||
|
new MemoryGame();
|
||||||
Reference in New Issue
Block a user