feat: 添加贪吃蛇和记忆配对纸牌游戏

- 贪吃蛇游戏: Canvas渲染、方向控制、碰撞检测、计分系统
- 记忆配对纸牌: 3D翻转动画、配对消除、计时器、胜利判定
This commit is contained in:
zhangqing
2026-01-16 23:34:10 +08:00
commit c4c48d17b4
4 changed files with 627 additions and 0 deletions
+209
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+192
View File
@@ -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();