commit c4c48d17b4fe62fc7a88cef08c725e865fcac058 Author: zhangqing Date: Fri Jan 16 23:34:10 2026 +0800 feat: 添加贪吃蛇和记忆配对纸牌游戏 - 贪吃蛇游戏: Canvas渲染、方向控制、碰撞检测、计分系统 - 记忆配对纸牌: 3D翻转动画、配对消除、计时器、胜利判定 diff --git a/game.js b/game.js new file mode 100644 index 0000000..5176493 --- /dev/null +++ b/game.js @@ -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(); + } +}); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..5e30f0f --- /dev/null +++ b/index.html @@ -0,0 +1,51 @@ + + + + + + 贪吃蛇大作战 + + + +
分数: 0
+
+ +
+ + + \ No newline at end of file diff --git a/memory.html b/memory.html new file mode 100644 index 0000000..d7bb592 --- /dev/null +++ b/memory.html @@ -0,0 +1,175 @@ + + + + + + 记忆配对纸牌 + + + +

记忆配对纸牌

+
+
步数: 0
+
时间: 0:00
+
配对: 0/8
+
+
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/memory.js b/memory.js new file mode 100644 index 0000000..aecc8d1 --- /dev/null +++ b/memory.js @@ -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 = ` +
+
${card.symbol}
+ `; + 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(); \ No newline at end of file