From 267c30cee03eab80d8523a0dbc9b2334c4910dc7 Mon Sep 17 00:00:00 2001 From: fengjiansun Date: Mon, 8 Dec 2025 20:48:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/css/main-snake.css | 10 ++ src/index.html | 159 ++++++++++++++++++ src/js/init.js | 50 ++++++ src/js/snake.js | 366 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 585 insertions(+) diff --git a/src/css/main-snake.css b/src/css/main-snake.css index 7938bf34..aa8eb4ab 100755 --- a/src/css/main-snake.css +++ b/src/css/main-snake.css @@ -85,6 +85,16 @@ a.snake-link:hover { position: absolute; } +.snake-bomb-block { + margin: 0px; + padding: 0px; + background-color: #ff0000; + border: 2px solid #000000; + position: absolute; + border-radius: 50%; + box-shadow: 0 0 10px rgba(255, 0, 0, 0.8); +} + .snake-playing-field { margin: 0px; padding: 0px; diff --git a/src/index.html b/src/index.html index 03dc6b10..be2016d4 100755 --- a/src/index.html +++ b/src/index.html @@ -26,6 +26,96 @@ z-index: 10000; padding: 5px; } + + /* 游戏说明面板样式 */ + .instructions-panel { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + z-index: 20000; + display: flex; + justify-content: center; + align-items: center; + } + + .instructions-content { + background-color: white; + padding: 30px; + border-radius: 10px; + max-width: 600px; + max-height: 80%; + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + } + + .instructions-content h2 { + margin-top: 0; + color: #333; + text-align: center; + } + + .instructions-content h3 { + color: #555; + margin-top: 20px; + margin-bottom: 10px; + } + + .instructions-content ul { + margin: 10px 0; + padding-left: 20px; + } + + .instructions-content li { + margin: 5px 0; + line-height: 1.5; + } + + .instructions-close { + display: block; + margin: 20px auto 0; + padding: 10px 20px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + } + + .instructions-close:hover { + background-color: #45a049; + } + + #show_instructions { + margin-left: 10px; + padding: 5px 10px; + background-color: #2196F3; + color: white; + border: none; + border-radius: 3px; + cursor: pointer; + } + + #show_instructions:hover { + background-color: #0b7dda; + } + + #pause_game { + margin-left: 10px; + padding: 5px 10px; + background-color: #ff9800; + color: white; + border: none; + border-radius: 3px; + cursor: pointer; + } + + #pause_game:hover { + background-color: #e68900; + } diff --git a/src/js/init.js b/src/js/init.js index daddd8b8..604f95fe 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -2,11 +2,17 @@ const mySnakeBoard = new SNAKE.Board({ boardContainer: "game-area", fullScreen: true, premoveOnPause: false, + difficulty: "medium", // 默认难度 onLengthUpdate: (length) => { console.log(`Length: ${length}`); }, onPauseToggle: (isPaused) => { console.log(`Is paused: ${isPaused}`); + // 更新暂停按钮文本 + const pauseButton = document.getElementById('pause_game'); + if (pauseButton) { + pauseButton.textContent = isPaused ? '继续' : '暂停'; + } }, onInit: (params) => { console.log("init!"); @@ -19,3 +25,47 @@ const mySnakeBoard = new SNAKE.Board({ console.log("dead!"); }, }); + +// 暂停按钮功能 +document.addEventListener('DOMContentLoaded', function() { + const pauseButton = document.getElementById('pause_game'); + if (pauseButton) { + pauseButton.addEventListener('click', function() { + if (mySnakeBoard && mySnakeBoard.getBoardState() === 2) { // 只有在游戏进行中才能暂停 + mySnakeBoard.setPaused(!mySnakeBoard.getPaused()); + } + }); + } + + // 难度选择功能 + const modeSelect = document.getElementById('selectMode'); + if (modeSelect) { + modeSelect.addEventListener('change', function() { + const selectedValue = this.value; + let difficulty = 'medium'; + + switch(selectedValue) { + case '100': // Easy + difficulty = 'easy'; + break; + case '75': // Medium + difficulty = 'medium'; + break; + case '50': // Hard + difficulty = 'hard'; + break; + case '25': // Impossible + difficulty = 'impossible'; + break; + case '110': // Rush + difficulty = 'rush'; + break; + } + + // 更新游戏板配置 + if (mySnakeBoard) { + mySnakeBoard.setDifficulty(difficulty); + } + }); + } +}); diff --git a/src/js/snake.js b/src/js/snake.js index 6232e8d9..8bbb74eb 100644 --- a/src/js/snake.js +++ b/src/js/snake.js @@ -412,6 +412,14 @@ SNAKE.Snake = setTimeout(function () { me.go(); }, snakeSpeed); + } else if ( + grid[newHead.row][newHead.col] === playingBoard.getGridBombValue() + ) { + // 蛇吃到炸弹,触发爆炸 + grid[newHead.row][newHead.col] = 1; + playingBoard.bombExploded(); + me.handleDeath(); + return; } }; @@ -555,6 +563,292 @@ SNAKE.Snake = }; })(); +/** + * This class manages bombs that will explode after a countdown. + * @class Bomb + * @constructor + * @namespace SNAKE + * @param {Object} config The configuration object for the class. Contains playingBoard (the SNAKE.Board that this bomb resides in). + */ + +SNAKE.Bomb = + SNAKE.Bomb || + (function () { + // ------------------------------------------------------------------------- + // Private static variables and methods + // ------------------------------------------------------------------------- + + let instanceNumber = 0; + + function getRandomPosition(x, y) { + return Math.floor(Math.random() * (y + 1 - x)) + x; + } + + // ------------------------------------------------------------------------- + // Contructor + public and private definitions + // ------------------------------------------------------------------------- + + /* + config options: + playingBoard - the SnakeBoard that this object belongs too. + difficulty - the difficulty level affecting explosion range + */ + return function (config) { + if (!config || !config.playingBoard) { + return; + } + + // ----- private variables ----- + + const me = this; + const playingBoard = config.playingBoard; + const difficulty = config.difficulty || 'medium'; + let bombRow, bombCol; + let countdown = 3; // 倒计时从3开始 + let isActive = false; + let countdownInterval; + const myId = instanceNumber++; + + const elmBomb = document.createElement("div"); + elmBomb.setAttribute("id", "snake-bomb-" + myId); + elmBomb.className = "snake-bomb-block"; + elmBomb.style.width = playingBoard.getBlockWidth() + "px"; + elmBomb.style.height = playingBoard.getBlockHeight() + "px"; + elmBomb.style.left = "-1000px"; + elmBomb.style.top = "-1000px"; + elmBomb.style.fontSize = "14px"; + elmBomb.style.fontWeight = "bold"; + elmBomb.style.color = "white"; + elmBomb.style.textAlign = "center"; + elmBomb.style.lineHeight = playingBoard.getBlockHeight() + "px"; + elmBomb.style.zIndex = getNextHighestZIndex({ tmp: { elm: elmBomb } }); + elmBomb.style.display = "none"; + playingBoard.getBoardContainer().appendChild(elmBomb); + + // 根据难度获取爆炸范围 + function getExplosionRange() { + switch (difficulty.toLowerCase()) { + case 'easy': + case 'rush': + return 1; + case 'hard': + return 3; + case 'impossible': + return 4; + case 'medium': + default: + return 2; + } + } + + // ----- public methods ----- + + /** + * @method getBombElement + * @return {DOM Element} The div that represents the bomb. + */ + me.getBombElement = function () { + return elmBomb; + }; + + /** + * @method isActive + * @return {Boolean} Whether the bomb is currently active. + */ + me.isActive = function () { + return isActive; + }; + + /** + * @method getPosition + * @return {Object} The row and column of the bomb. + */ + me.getPosition = function () { + return { row: bombRow, col: bombCol }; + }; + + /** + * Places the bomb at a random location and starts countdown. + * @method placeBomb + * @return {Boolean} Whether a bomb was able to spawn (true) or not (false). + */ + me.placeBomb = function () { + // 如果炸弹已激活,先清除它 + if (isActive) { + me.defuseBomb(); + } + + let row = 0, + col = 0, + numTries = 0; + + const maxRows = playingBoard.grid.length - 1; + const maxCols = playingBoard.grid[0].length - 1; + + // 找到一个空位置(不能是蛇身或食物) + while (playingBoard.grid[row][col] !== 0) { + row = getRandomPosition(1, maxRows); + col = getRandomPosition(1, maxCols); + + numTries++; + if (numTries > 20000) { + return false; // 找不到合适位置 + } + } + + // 在网格上标记炸弹位置(使用正值避免与食物冲突) + playingBoard.grid[row][col] = 10; + bombRow = row; + bombCol = col; + isActive = true; + countdown = 3; + + elmBomb.style.top = row * playingBoard.getBlockHeight() + "px"; + elmBomb.style.left = col * playingBoard.getBlockWidth() + "px"; + elmBomb.innerHTML = countdown; + elmBomb.style.backgroundColor = "red"; + elmBomb.style.display = "block"; + + // 开始倒计时 + countdownInterval = setInterval(function () { + countdown--; + if (countdown > 0) { + elmBomb.innerHTML = countdown; + // 根据倒计时改变颜色 + if (countdown === 2) { + elmBomb.style.backgroundColor = "orange"; + } else if (countdown === 1) { + elmBomb.style.backgroundColor = "yellow"; + elmBomb.style.color = "black"; + // 在最后一秒显示爆炸范围 + me.showExplosionRange(); + } + } else { + me.explode(); + } + }, 1000); + + return true; + }; + + /** + * Handles bomb explosion. + * @method explode + */ + me.explode = function () { + if (!isActive) return; + + clearInterval(countdownInterval); + const range = getExplosionRange(); + const explosionPositions = []; + + // 计算爆炸影响的位置 + for (let r = bombRow - range; r <= bombRow + range; r++) { + for (let c = bombCol - range; c <= bombCol + range; c++) { + // 检查边界 + if (r >= 0 && r < playingBoard.grid.length && + c >= 0 && c < playingBoard.grid[0].length) { + explosionPositions.push({ row: r, col: c }); + } + } + } + + // 视觉爆炸效果 + elmBomb.style.backgroundColor = "white"; + elmBomb.innerHTML = "💥"; + elmBomb.style.fontSize = "20px"; + + // 通知游戏板处理爆炸后果 + if (playingBoard.handleBombExplosion) { + playingBoard.handleBombExplosion(explosionPositions); + } + + // 延迟清除爆炸效果 + setTimeout(function () { + me.defuseBomb(); + }, 500); + }; + + /** + * Removes the bomb from the board. + * @method defuseBomb + */ + me.defuseBomb = function () { + if (!isActive) return; + + clearInterval(countdownInterval); + + // 清除网格标记 + if (playingBoard.grid[bombRow] && playingBoard.grid[bombRow][bombCol] === 10) { + playingBoard.grid[bombRow][bombCol] = 0; + } + + elmBomb.style.left = "-1000px"; + elmBomb.style.top = "-1000px"; + elmBomb.innerHTML = ""; + elmBomb.style.backgroundColor = ""; + elmBomb.style.color = "white"; + elmBomb.style.fontSize = "14px"; + elmBomb.style.display = "none"; + + isActive = false; + }; + + /** + * Gets the explosion range for current difficulty. + * @method getExplosionRange + * @return {Number} The explosion range. + */ + me.getExplosionRange = function () { + return getExplosionRange(); + }; + + /** + * Shows explosion range preview. + * @method showExplosionRange + */ + me.showExplosionRange = function () { + if (!isActive) return; + + const range = getExplosionRange(); + const blockWidth = playingBoard.getBlockWidth(); + const blockHeight = playingBoard.getBlockHeight(); + + // 创建爆炸范围的视觉提示 + for (let r = bombRow - range; r <= bombRow + range; r++) { + for (let c = bombCol - range; c <= bombCol + range; c++) { + // 检查边界 + if (r >= 0 && r < playingBoard.grid.length && + c >= 0 && c < playingBoard.grid[0].length) { + + // 创建临时的高亮效果 + const highlight = document.createElement("div"); + highlight.className = "bomb-range-highlight"; + highlight.style.position = "absolute"; + highlight.style.top = r * blockHeight + "px"; + highlight.style.left = c * blockWidth + "px"; + highlight.style.width = blockWidth + "px"; + highlight.style.height = blockHeight + "px"; + highlight.style.backgroundColor = "rgba(255, 255, 0, 0.3)"; + highlight.style.border = "1px solid rgba(255, 255, 0, 0.8)"; + highlight.style.zIndex = parseInt(elmBomb.style.zIndex) - 1; + highlight.style.pointerEvents = "none"; + + playingBoard.getBoardContainer().appendChild(highlight); + + // 1秒后移除高亮 + setTimeout(function () { + if (highlight.parentNode) { + highlight.parentNode.removeChild(highlight); + } + }, 1000); + } + } + } + }; + }; + })(); + /** * This class manages the food which the snake will eat. * @class Food @@ -757,6 +1051,7 @@ SNAKE.Board = const blockWidth = 20; const blockHeight = 20; const GRID_FOOD_VALUE = -1; // the value of a spot on the board that represents snake food; MUST BE NEGATIVE + const GRID_BOMB_VALUE = -2; // the value of a spot on the board that represents a bomb; MUST BE NEGATIVE // defaults if (!config.onLengthUpdate) { @@ -775,6 +1070,7 @@ SNAKE.Board = let myFood, mySnake, + myBomb, boardState = BOARD_READY, // 0: in active, 1: awaiting game start, 2: playing game myKeyListener, myWindowListener, @@ -887,6 +1183,7 @@ SNAKE.Board = moveSnakeWithAI: config.moveSnakeWithAI, }); myFood = new SNAKE.Food({ playingBoard: me }); + myBomb = new SNAKE.Bomb({ playingBoard: me, difficulty: config.difficulty }); if (elmWelcome) { elmWelcome.style.zIndex = 1000; @@ -1038,6 +1335,9 @@ SNAKE.Board = false, ); mySnake.reset(); + if (myBomb) { + myBomb.defuseBomb(); + } config.onLengthUpdate(1); elmLengthPanel.innerHTML = "Length: 1"; me.setupPlayingField(); @@ -1066,6 +1366,10 @@ SNAKE.Board = me.getGridFoodValue = function () { return GRID_FOOD_VALUE; }; + + me.getGridBombValue = function () { + return GRID_BOMB_VALUE; + }; /** * @method getPlayingFieldElement * @return {DOM Element} The div representing the playing field (this is where the snake can move). @@ -1325,6 +1629,10 @@ SNAKE.Board = if (!myFood.randomlyPlaceFood()) { return false; } + // 随机生成炸弹(20%概率) + if (Math.random() < 0.2 && mySnake.snakeLength > 5) { + myBomb.placeBomb(); + } return true; }; @@ -1337,6 +1645,46 @@ SNAKE.Board = config.onDeath({ startAIGame: me.startAIGame }); }; + /** + * This method is called when a bomb explodes. + * @method bombExploded + */ + me.bombExploded = function () { + if (myBomb) { + myBomb.explode(); + } + }; + + /** + * This method handles bomb explosion effects. + * @method handleBombExplosion + * @param {Array} explosionPositions - Array of positions affected by explosion + */ + me.handleBombExplosion = function (explosionPositions) { + // 清除爆炸范围内的食物 + if (myFood) { + const foodPos = myFood.getPosition(); + const foodHit = explosionPositions.some(pos => + pos.row === foodPos.row && pos.col === foodPos.col + ); + if (foodHit) { + myFood.randomlyPlaceFood(); + } + } + + // 检查蛇是否被爆炸击中 + if (mySnake) { + const snakeHead = mySnake.snakeHead; + const headHit = explosionPositions.some(pos => + pos.row === snakeHead.row && pos.col === snakeHead.col + ); + if (headHit) { + me.handleDeath(); + return; + } + } + }; + /** * This method is called when the snake wins. * @method handleWin @@ -1352,6 +1700,24 @@ SNAKE.Board = me.getSpeed = () => { return mySnake.getSpeed(); }; + + /** + * Sets the difficulty level. + * @method setDifficulty + * @param {String} difficulty The difficulty level (easy, medium, hard, impossible, rush). + */ + me.setDifficulty = (difficulty) => { + config.difficulty = difficulty; + // 如果炸弹存在,更新其难度 + if (myBomb) { + // 重新创建炸弹以应用新难度 + myBomb.defuseBomb(); + myBomb = new SNAKE.Bomb({ + playingBoard: me, + difficulty: difficulty + }); + } + }; me.startAIGame = () => { me.resetBoard();