Last active
April 28, 2025 08:43
-
-
Save andrejsharapov/8e88dccd2960b117689f797b3af268e4 to your computer and use it in GitHub Desktop.
Spy Maze Game
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Шпионский лабиринт v1.1.0</title> | |
| <style> | |
| body { | |
| background-color: #222; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100vh; | |
| margin: 0; | |
| flex-direction: column; | |
| color: white; | |
| font-family: sans-serif; | |
| } | |
| #gameCanvas { | |
| display: none; | |
| } | |
| #hud { | |
| color: white; | |
| font-family: sans-serif; | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| left: 10px; | |
| font-size: 20px; | |
| display: none; | |
| grid-template-columns: repeat(3, 1fr); | |
| place-items: center; | |
| text-align: center; | |
| } | |
| #controls { | |
| margin-top: 10px; | |
| display: flex; | |
| gap: 10px; | |
| display: none; | |
| } | |
| button { | |
| background-color: #4CAF50; | |
| border: none; | |
| color: white; | |
| padding: 10px 20px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| } | |
| #levelCompleteScreen, | |
| #gameOverScreen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| flex-direction: column; | |
| color: white; | |
| } | |
| #levelCompleteScreen button, | |
| #gameOverScreen button { | |
| margin-top: 20px; | |
| } | |
| #pausePanel { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background-color: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| font-size: 24px; | |
| display: none; | |
| } | |
| #startScreen { | |
| text-align: center; | |
| } | |
| #startScreen h1 { | |
| font-size: 3em; | |
| margin-bottom: 20px; | |
| } | |
| #startScreen p { | |
| font-size: 1.2em; | |
| line-height: 1.5; | |
| margin-bottom: 30px; | |
| max-width: 800px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Экран приветствия --> | |
| <div id="startScreen"> | |
| <h1>Шпионский лабиринт</h1> | |
| <p> | |
| Добро пожаловать в Шпионский лабиринт, агент! Твоя миссия — собрать необходимые сведения и передать их в штаб. Ты | |
| должен добраться до него незамеченным. Я отметил его синей меткой. Избегай разведчиков противника, иначе будет | |
| худо. Добравшись до штаба ты получишь очки репутации и новое задание. Имей ввиду, каждое новое задание будет | |
| сложнее предыдущего, ты не должен сдаваться! На каждое задание у тебя есть 5 попыток, но имей ввиду, за | |
| каждое ранение ты теряешь 100 очков. Используй клавиши со стрелками для перемещения. | |
| Удачи, агент! | |
| </p> | |
| <button id="startGameButton">Вперёд!</button> | |
| </div> | |
| <div id="hud"> | |
| <div> | |
| <span id="lives">❤❤❤❤❤</span> | |
| </div> | |
| <div> | |
| Время: <span id="time">00:00</span> | |
| | Уровень: <span id="level">1</span> | |
| <p>Собрано сведений: <span id="collectedPieces">0</span>/5</p> | |
| </div> | |
| <div> | |
| 🏆 <span id="score"> 1000</span> | |
| </div> | |
| </div> | |
| <canvas id="gameCanvas"></canvas> | |
| <div id="controls"> | |
| <button id="pauseButton">Пауза</button> | |
| </div> | |
| <div id="pausePanel"> | |
| Игра остановлена | |
| </div> | |
| <!-- Экран "Game Over" --> | |
| <div id="gameOverScreen"> | |
| <h2>Ты не справился! </h2> | |
| <p>Не отчаивайся, я уверен, у тебя всё получится в следующий раз!</p> | |
| <button id="restartButton">Начать заново</button> | |
| </div> | |
| <div id="levelCompleteScreen"> | |
| <h2>Этап пройден!</h2> | |
| <p>Готов к следующему заданию?</p> | |
| <button id="nextLevelButton">Перейти</button> | |
| </div> | |
| <script> | |
| // === 1. Настройки === | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const canvasWidth = window.innerWidth * 0.7; | |
| const canvasHeight = window.innerHeight * 0.7; | |
| canvas.width = canvasWidth; | |
| canvas.height = canvasHeight; | |
| const mazeWidth = 40; | |
| const mazeHeight = 20; | |
| const cellSize = Math.min(canvasWidth / mazeWidth, canvasHeight / mazeHeight); | |
| // === 2. Данные игры === | |
| const treeEmojis = ["🌲", "🌳"]; | |
| const heartSpawnChance = 0.5; // Вероятность появления сердечка (50%) | |
| let maze = []; | |
| let treeMap = []; // Добавляем массив для хранения выбранных деревьев | |
| let heart = null; // Переменная для хранения координат сердечка | |
| let paperPieces = []; // Массив для хранения координат кусочков бумаги | |
| let collectedPieces = 0; // Количество собранных кусочков бумаги | |
| let spy = { x: 1, y: 1, radius: cellSize / 2 - 2, color: 'blue' }; | |
| let guards = []; | |
| let gameWon = false; | |
| let gameOver = false; | |
| let gamePaused = true; | |
| let currentLevel = 1; | |
| let maxLevels = 10; | |
| let playerLives = 5; | |
| let playerScore = 500; | |
| let startTime; | |
| let elapsedTime = 0; | |
| let pauseStartTime = 0; | |
| const themes = { | |
| "forest": { | |
| wallColor: "#228B22", | |
| pathColor: "#DEB887", | |
| exitColor: "#3b6ad1", | |
| spyColor: "#006400", | |
| guardColor: "#800000" | |
| }, | |
| "dungeon": { | |
| wallColor: "gray", | |
| pathColor: "#555", | |
| exitColor: "brown", | |
| spyColor: "darkblue", | |
| guardColor: "firebrick" | |
| } | |
| }; | |
| let currentTheme = "forest"; // Текущая тема по умолчанию | |
| // Новые переменные для щита | |
| let shield = null; // Координаты щита на карте (или null, если щита нет) | |
| const shieldSpawnChance = 0.8; // Вероятность появления щита на уровне (30%) | |
| let shieldActive = false; // Активен ли щит в данный момент | |
| let shieldTimer = 0; // Сколько времени осталось до конца действия щита | |
| const shieldDuration = 15; // Длительность действия щита в секундах | |
| // === 3. Кнопки === | |
| const pauseButton = document.getElementById('pauseButton'); | |
| const nextLevelButton = document.getElementById('nextLevelButton'); | |
| const levelCompleteScreen = document.getElementById('levelCompleteScreen'); | |
| const pausePanel = document.getElementById('pausePanel'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const startGameButton = document.getElementById('startGameButton'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const restartButton = document.getElementById('restartButton'); | |
| // === 4. Функции интерфейса === | |
| function generateHearts(lives, maxLives) { | |
| let hearts = ''; | |
| for (let i = 0; i < lives; i++) { | |
| hearts += '❤️'; | |
| } | |
| for (let i = 0; i < maxLives - lives; i++) { | |
| hearts += '💔'; | |
| } | |
| return hearts; | |
| } | |
| function generateHeart() { | |
| let x, y; | |
| do { | |
| x = Math.floor(Math.random() * mazeWidth); | |
| y = Math.floor(Math.random() * mazeHeight); | |
| } while (maze[y][x] !== 0 || (x === 1 && y === 1) || (x == mazeWidth - 2 && y == mazeHeight - 2)); // Убеждаемся, что сердечко не появляется на стене или на старте или на финише | |
| heart = { x: x, y: y }; | |
| } | |
| function formatTime(seconds) { | |
| const minutes = Math.floor(seconds / 60); | |
| const remainingSeconds = Math.floor(seconds % 60); | |
| const formattedMinutes = String(minutes).padStart(2, '0'); | |
| const formattedSeconds = String(remainingSeconds).padStart(2, '0'); | |
| return `${formattedMinutes}:${formattedSeconds}`; | |
| } | |
| // === 5. Обработчики событий === | |
| pauseButton.addEventListener('click', togglePause); | |
| nextLevelButton.addEventListener('click', continueToNextLevel); | |
| startGameButton.addEventListener('click', showGame); | |
| restartButton.addEventListener('click', restartGame); | |
| function showGame() { | |
| startScreen.style.display = 'none'; | |
| canvas.style.display = 'block'; | |
| hud.style.display = 'grid'; | |
| controls.style.display = 'flex'; | |
| gamePaused = false | |
| init(); | |
| gameLoop(); | |
| startTime = Date.now(); | |
| elapsedTime = 0; | |
| } | |
| function togglePause() { | |
| gamePaused = !gamePaused; | |
| pausePanel.style.display = gamePaused ? 'block' : 'none'; | |
| if (gamePaused) { | |
| pauseStartTime = Date.now(); // Запоминаем время постановки на паузу | |
| } else { | |
| startTime += (Date.now() - pauseStartTime); // Корректируем startTime | |
| } | |
| } | |
| function continueToNextLevel() { | |
| levelCompleteScreen.style.display = 'none'; | |
| levelComplete(); | |
| } | |
| function restartGame() { | |
| gameOverScreen.style.display = 'none'; | |
| showGame() | |
| } | |
| // Функция для активации щита | |
| function generateShield() { | |
| let x, y; | |
| do { | |
| x = Math.floor(Math.random() * mazeWidth); | |
| y = Math.floor(Math.random() * mazeHeight); | |
| } while (maze[y][x] !== 0 || (x === 1 && y === 1) || (x == mazeWidth - 2 && y == mazeHeight - 2)); // Убеждаемся, что щит не появляется на стене, старте или финише | |
| shield = { x: x, y: y }; | |
| } | |
| // Функции генерации лабиринта | |
| function generateMaze() { | |
| maze = []; | |
| treeMap = []; // Инициализируем treeMap | |
| for (let y = 0; y < mazeHeight; y++) { | |
| maze[y] = []; | |
| treeMap[y] = []; // Инициализируем treeMap[y] | |
| for (let x = 0; x < mazeWidth; x++) { | |
| maze[y][x] = 1; // 1 означает стену | |
| treeMap[y][x] = treeEmojis[Math.floor(Math.random() * treeEmojis.length)]; // Выбираем случайное дерево и сохраняем в treeMap | |
| } | |
| } | |
| function recursiveBacktracker(x, y) { | |
| maze[y][x] = 0; // 0 означает проход | |
| const directions = shuffle([ | |
| { dx: -2, dy: 0 }, | |
| { dx: 2, dy: 0 }, | |
| { dx: 0, dy: -2 }, | |
| { dx: 0, dy: 2 } | |
| ]); | |
| for (const dir of directions) { | |
| const newX = x + dir.dx; | |
| const newY = y + dir.dy; | |
| if (newX > 0 && newX < mazeWidth - 1 && newY > 0 && newY < mazeHeight - 1 && maze[newY][newX] === 1) { | |
| maze[y + dir.dy / 2][x + dir.dx / 2] = 0; | |
| recursiveBacktracker(newX, newY); | |
| } | |
| } | |
| } | |
| const startX = 1; | |
| const startY = 1; | |
| recursiveBacktracker(startX, startY); | |
| const extraPassages = mazeWidth * mazeHeight * 0.05; | |
| for (let i = 0; i < extraPassages; i++) { | |
| const x = Math.floor(Math.random() * (mazeWidth - 2)) + 1; | |
| const y = Math.floor(Math.random() * (mazeHeight - 2)) + 1; | |
| maze[y][x] = 0; | |
| } | |
| return maze; | |
| } | |
| function shuffle(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 placeGuards() { | |
| guards = []; | |
| let numGuards = 1 + (currentLevel * 2); | |
| for (let i = 0; i < numGuards; i++) { | |
| let guardX, guardY; | |
| do { | |
| guardX = Math.floor(Math.random() * mazeWidth); | |
| guardY = Math.floor(Math.random() * mazeHeight); | |
| } while (maze[guardY][guardX] !== 0 || (guardX === 1 && guardY === 1)); | |
| let directions = ["left", "right", "up", "down"]; | |
| let randomDirection = directions[Math.floor(Math.random() * directions.length)]; | |
| guards.push({ | |
| x: guardX, | |
| y: guardY, | |
| radius: cellSize / 2 - 2, | |
| color: 'red', | |
| direction: randomDirection, | |
| visionRadius: 5, | |
| isChasing: false, | |
| moveTimer: 0, | |
| moveInterval: 550 | |
| }); | |
| } | |
| } | |
| // === 7. Функции отрисовки === | |
| function drawMaze() { | |
| const theme = themes[currentTheme]; | |
| for (let y = 0; y < mazeHeight; y++) { | |
| for (let x = 0; x < mazeWidth; x++) { | |
| const tile = maze[y][x]; | |
| if (tile === 1) { | |
| ctx.fillStyle = theme.wallColor; | |
| ctx.font = cellSize + "px Arial"; | |
| ctx.fillText(treeMap[y][x], x * cellSize, y * cellSize + cellSize - 2); // Используем дерево из treeMap | |
| } else if (tile === 2) { | |
| ctx.fillStyle = theme.exitColor; | |
| ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize) | |
| } else { | |
| ctx.fillStyle = theme.pathColor; | |
| ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); | |
| } | |
| } | |
| // Отрисовываем доп. жизнь | |
| if (heart) { | |
| ctx.font = cellSize + "px Arial"; | |
| ctx.fillStyle = "red"; | |
| ctx.fillText("💖", heart.x * cellSize, heart.y * cellSize + cellSize - 2); | |
| } | |
| // Отрисовываем щит | |
| if (shield) { | |
| ctx.font = cellSize + "px Arial"; | |
| ctx.fillStyle = "blue"; | |
| ctx.fillText("🛡️", shield.x * cellSize, shield.y * cellSize + cellSize - 2); | |
| } | |
| // Отрисовываем кусочки бумаги | |
| ctx.font = cellSize + "px Arial"; | |
| ctx.fillStyle = "white"; | |
| for (let i = 0; i < paperPieces.length; i++) { | |
| const piece = paperPieces[i]; | |
| ctx.fillText("📑", piece.x * cellSize, piece.y * cellSize + cellSize - 2); | |
| } | |
| } | |
| } | |
| function drawCircle(x, y, radius, color) { | |
| ctx.beginPath(); | |
| ctx.arc(x * cellSize + cellSize / 2, y * cellSize + cellSize / 2, radius, 0, Math.PI * 2); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| } | |
| function drawSpy() { | |
| const theme = themes[currentTheme]; | |
| drawCircle(spy.x, spy.y, spy.radius, theme.spyColor); // Используем цвет шпиона из текущей темы | |
| // Визуальное отображение щита | |
| if (shieldActive) { | |
| ctx.beginPath(); | |
| ctx.arc(spy.x * cellSize + cellSize / 2, spy.y * cellSize + cellSize / 2, spy.radius + 5, 0, Math.PI * 2); | |
| ctx.strokeStyle = "yellow"; | |
| ctx.lineWidth = 3; | |
| ctx.stroke(); | |
| } | |
| } | |
| function drawGuards() { | |
| const theme = themes[currentTheme]; | |
| for (let i = 0; i < guards.length; i++) { | |
| const guard = guards[i]; | |
| drawCircle(guard.x, guard.y, guard.radius, guard.color); | |
| //Отрисовываем радиус видимости | |
| ctx.beginPath(); | |
| ctx.arc(guard.x * cellSize + cellSize / 2, guard.y * cellSize + cellSize / 2, guard.visionRadius * cellSize, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(255, 0, 0, 0.1)'; // Цвет радиуса | |
| ctx.fill(); | |
| // Отрисовываем линию, указывающую направление | |
| ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; // Цвет линии | |
| ctx.lineWidth = 2; // Толщина линии | |
| ctx.beginPath(); | |
| ctx.moveTo(guard.x * cellSize + cellSize / 2, guard.y * cellSize + cellSize / 2); | |
| let lineEndX = guard.x; | |
| let lineEndY = guard.y; | |
| if (guard.direction === "left") lineEndX--; | |
| else if (guard.direction === "right") lineEndX++; | |
| else if (guard.direction === "up") lineEndY--; | |
| else if (guard.direction === "down") lineEndY++; | |
| ctx.lineTo(lineEndX * cellSize + cellSize / 2, lineEndY * cellSize + cellSize / 2); | |
| ctx.stroke(); | |
| } | |
| } | |
| function changeTheme(themeName) { | |
| if (themes[themeName]) { | |
| currentTheme = themeName; | |
| //initLevel(); // Перерисовываем лабиринт с новой темой | |
| } | |
| } | |
| function drawGameOver() { | |
| gameOverScreen.style.display = 'flex'; | |
| } | |
| function drawWin() { | |
| ctx.font = "40px Arial"; | |
| ctx.fillStyle = "green"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText("You Win!", canvasWidth / 2, canvasHeight / 2); | |
| } | |
| // === 8. Логика игры === | |
| let nextMove = null; | |
| document.addEventListener("keydown", function (event) { | |
| if (event.code === "KeyE") { | |
| if (maze[spy.y][spy.x] === 2) { | |
| continueToNextLevel(); | |
| } | |
| } | |
| }); | |
| document.addEventListener('keydown', function (event) { | |
| if (gamePaused) return; | |
| switch (event.key) { | |
| case 'ArrowUp': nextMove = 'up'; break; | |
| case 'ArrowDown': nextMove = 'down'; break; | |
| case 'ArrowLeft': nextMove = 'left'; break; | |
| case 'ArrowRight': nextMove = 'right'; break; | |
| case 'Enter': | |
| if (gameOver) { | |
| restartGame(); | |
| } | |
| break; | |
| } | |
| moveSpy(); | |
| }); | |
| function canSee(guard, targetX, targetY) { | |
| const dx = targetX - guard.x; | |
| const dy = targetY - guard.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance > guard.visionRadius) return false; | |
| let startX = guard.x; | |
| let startY = guard.y; | |
| let endX = targetX; | |
| let endY = targetY; | |
| let x = startX; | |
| let y = startY; | |
| while (x !== endX || y !== endY) { | |
| if (x < endX) x++; | |
| else if (x > endX) x--; | |
| if (y < endY) y++; | |
| else if (y > endY) y--; | |
| if (maze[y][x] === 1) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| function aStar(startX, startY, endX, endY, maze) { | |
| const openSet = [{ x: startX, y: startY, g: 0, h: heuristic(startX, startY, endX, endY) }]; | |
| const closedSet = []; | |
| function heuristic(x, y, endX, endY) { | |
| return Math.abs(x - endX) + Math.abs(y - endY); | |
| } | |
| function reconstructPath(node) { | |
| const path = []; | |
| while (node.parent) { | |
| path.push({ x: node.x, y: node.y }); | |
| node = node.parent; | |
| } | |
| return path.reverse(); | |
| } | |
| while (openSet.length > 0) { | |
| // Find the node with the lowest f = g + h value | |
| let current = openSet[0]; | |
| let currentIndex = 0; | |
| for (let i = 1; i < openSet.length; i++) { | |
| if (openSet[i].g + openSet[i].h < current.g + current.h) { | |
| current = openSet[i]; | |
| currentIndex = i; | |
| } | |
| } | |
| // If found the goal | |
| if (current.x === endX && current.y === endY) { | |
| return reconstructPath(current); | |
| } | |
| // Remove the current node from the open set and add it to the closed set | |
| openSet.splice(currentIndex, 1); | |
| closedSet.push(current); | |
| // Generate neighbors (adjacent cells) | |
| const neighbors = [ | |
| { x: current.x - 1, y: current.y }, | |
| { x: current.x + 1, y: current.y }, | |
| { x: current.x, y: current.y - 1 }, | |
| { x: current.x, y: current.y + 1 } | |
| ]; | |
| for (const neighbor of neighbors) { | |
| // Check if neighbor is within the maze bounds and is not a wall | |
| if (neighbor.x >= 0 && neighbor.x < mazeWidth && neighbor.y >= 0 && neighbor.y < mazeHeight && maze[neighbor.y][neighbor.x] === 0) { | |
| // Check if neighbor is already in the closed set | |
| const inClosedSet = closedSet.find(node => node.x === neighbor.x && node.y === neighbor.y); | |
| if (inClosedSet) continue; | |
| // Calculate g, h and f values | |
| const g = current.g + 1; | |
| const h = heuristic(neighbor.x, neighbor.y, endX, endY); | |
| // Check if neighbor is already in the open set | |
| let inOpenSet = openSet.find(node => node.x === neighbor.x && node.y === neighbor.y); | |
| if (!inOpenSet) { | |
| // Add neighbor to the open set | |
| neighbor.g = g; | |
| neighbor.h = h; | |
| neighbor.parent = current; | |
| openSet.push(neighbor); | |
| } else if (g < inOpenSet.g) { | |
| // This path to neighbor is better than the previous one | |
| inOpenSet.g = g; | |
| inOpenSet.parent = current; | |
| } | |
| } | |
| } | |
| } | |
| // If no path is found, return an empty array | |
| return []; | |
| } | |
| function moveGuards(deltaTime) { | |
| if (gamePaused) return; | |
| for (let i = 0; i < guards.length; i++) { | |
| const guard = guards[i]; | |
| guard.moveTimer += deltaTime; | |
| if (guard.moveTimer >= guard.moveInterval) { | |
| guard.moveTimer -= guard.moveInterval; | |
| if (canSee(guard, spy.x, spy.y)) { | |
| guard.isChasing = true; | |
| // Обновляем направление движения врага при начале преследования | |
| if (spy.x > guard.x) guard.direction = "right"; | |
| else if (spy.x < guard.x) guard.direction = "left"; | |
| else if (spy.y > guard.y) guard.direction = "down"; | |
| else if (spy.y < guard.y) guard.direction = "up"; | |
| } else { | |
| guard.isChasing = false; | |
| } | |
| if (guard.isChasing) { | |
| const path = aStar(guard.x, guard.y, spy.x, spy.y, maze); | |
| if (path.length > 0) { | |
| const nextX = path[0].x; | |
| const nextY = path[0].y; | |
| if (maze[nextY][nextX] === 0) { | |
| guard.x = nextX; | |
| guard.y = nextY; | |
| if (nextX > guard.x) guard.direction = "right"; | |
| else if (nextX < guard.x) guard.direction = "left"; | |
| else if (nextY > guard.y) guard.direction = "down"; | |
| else if (nextY < guard.y) guard.direction = "up"; | |
| } | |
| } | |
| } else { | |
| let dx = 0; | |
| let dy = 0; | |
| if (guard.direction === "left") dx = -1; | |
| else if (guard.direction === "right") dx = 1; | |
| else if (guard.direction === "up") dy = -1; | |
| else if (guard.direction === "down") dy = 1; | |
| if (guard.y + dy >= 0 && guard.y + dy < mazeHeight && | |
| guard.x + dx >= 0 && guard.x + dx < mazeWidth && | |
| maze[guard.y + dy][guard.x + dx] === 0) { | |
| guard.x += dx; | |
| guard.y += dy; | |
| } else { | |
| // Try to find a valid direction | |
| let possibleDirections = ["left", "right", "up", "down"]; | |
| let newDirectionFound = false; | |
| for (let attempt = 0; attempt < 4; attempt++) { | |
| let newDirection = possibleDirections[Math.floor(Math.random() * possibleDirections.length)]; | |
| dx = 0; | |
| dy = 0; | |
| if (newDirection === "left") dx = -1; | |
| else if (newDirection === "right") dx = 1; | |
| else if (newDirection === "up") dy = -1; | |
| else if (newDirection === "down") dy = 1; | |
| if (guard.y + dy >= 0 && guard.y + dy < mazeHeight && | |
| guard.x + dx >= 0 && guard.x + dx < mazeWidth && | |
| maze[guard.y + dy][guard.x + dx] === 0) { | |
| guard.direction = newDirection; | |
| newDirectionFound = true; | |
| guard.x += dx; | |
| guard.y += dy; | |
| break; | |
| } | |
| } | |
| // If a valid direction cannot be found, do nothing | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function moveSpy() { | |
| if (gamePaused) return; | |
| if (nextMove) { | |
| let newX = spy.x; | |
| let newY = spy.y; | |
| switch (nextMove) { | |
| case 'up': newY--; break; | |
| case 'down': newY++; break; | |
| case 'left': newX--; break; | |
| case 'right': newX++; break; | |
| } | |
| if (maze[newY]?.[newX] === 0 || maze[newY]?.[newX] === 2) { | |
| spy.x = newX; | |
| spy.y = newY; | |
| } | |
| nextMove = null; | |
| } | |
| } | |
| function checkCollisions() { | |
| if (gamePaused) return; | |
| for (let i = guards.length - 1; i >= 0; i--) { | |
| const guard = guards[i]; | |
| const dx = spy.x * cellSize - guard.x * cellSize; | |
| const dy = spy.y * cellSize - guard.y * cellSize; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < spy.radius + guard.radius) { | |
| if (shieldActive) { | |
| guards.splice(i, 1); // Щит уничтожает охранника | |
| continue; // Пропускаем нанесение урона | |
| } else { | |
| playerScore -= 100; | |
| document.getElementById("score").innerHTML = playerScore; | |
| playerLives--; | |
| document.getElementById("lives").innerHTML = generateHearts(playerLives, 5); | |
| guards.splice(i, 1); // Удаляем охранника из массива | |
| continue; // Переходим к следующему охраннику | |
| } | |
| } | |
| } | |
| if (playerLives <= 0) { | |
| gameOver = true; | |
| drawGameOver() | |
| return; | |
| } | |
| // Проверяем столкновение с щитом | |
| if (shield && spy.x >= shield.x && spy.x <= shield.x + 1 && spy.y >= shield.y && spy.y <= shield.y + 1) { | |
| console.log("Подобрали щит!"); | |
| shieldActive = true; | |
| shieldTimer = shieldDuration; | |
| shield = null; // Удаляем щит с карты | |
| } | |
| // Пройдите лабиринт, чтобы найти выход. | |
| for (let y = 0; y < mazeHeight; y++) { | |
| for (let x = 0; x < mazeWidth; x++) { | |
| if (maze[y][x] === 2) { | |
| //Check if the spy is on the exit | |
| if (spy.x === x && spy.y === y) { | |
| showLevelCompleteScreen(); | |
| return; //Exit the function after showing level complete screen | |
| } | |
| } | |
| } | |
| } | |
| if (heart && spy.x >= heart.x && spy.x <= heart.x + 1 && spy.y >= heart.y && spy.y <= heart.y + 1) { | |
| playerLives++; | |
| document.getElementById("lives").innerHTML = generateHearts(playerLives, 5); | |
| heart = null; | |
| } | |
| // Проверяем столкновение с кусочками бумаги | |
| for (let i = 0; i < paperPieces.length; i++) { | |
| const piece = paperPieces[i]; | |
| if (spy.x >= piece.x && spy.x <= piece.x + 1 && spy.y >= piece.y && spy.y <= piece.y + 1) { | |
| paperPieces.splice(i, 1); // Удаляем собранный кусочек | |
| collectedPieces++; // Увеличиваем количество собранных кусочков | |
| document.getElementById("collectedPieces").innerText = collectedPieces; // Обновляем значение элемента | |
| break; | |
| } | |
| } | |
| } | |
| function showLevelCompleteScreen() { | |
| if (collectedPieces === 5) { | |
| gamePaused = true; | |
| levelCompleteScreen.style.display = 'flex'; | |
| } | |
| } | |
| function levelComplete() { | |
| playerScore += 500; | |
| document.getElementById("score").innerText = playerScore; | |
| currentLevel++; | |
| if (currentLevel > maxLevels) { | |
| gameWon = true; | |
| } else { | |
| playerLives = 5; | |
| document.getElementById("lives").innerHTML = generateHearts(playerLives, 5); | |
| document.getElementById("level").innerText = currentLevel; | |
| initLevel(); | |
| gamePaused = false | |
| } | |
| } | |
| function initLevel() { | |
| maze = generateMaze(); | |
| placeGuards(); | |
| spy = { x: 1, y: 1, radius: cellSize / 2 - 2, color: 'blue' }; | |
| lastTime = performance.now(); | |
| for (let i = 0; i < guards.length; i++) { | |
| guards[i].isChasing = false | |
| } | |
| if (currentLevel >= 2 && Math.random() < heartSpawnChance) { | |
| generateHeart(); | |
| } | |
| // Генерируем щит | |
| if (currentLevel >= 1 && Math.random() < shieldSpawnChance) { | |
| generateShield(); | |
| } | |
| generatePaperPieces(); // Генерируем кусочки бумаги | |
| collectedPieces = 0; // Сбрасываем количество собранных кусочков | |
| generateExit(); | |
| } | |
| function init() { | |
| currentLevel = 1; | |
| playerLives = 5; | |
| playerScore = 500; | |
| document.getElementById("lives").innerHTML = generateHearts(playerLives, 5); | |
| document.getElementById("level").innerText = currentLevel; | |
| document.getElementById("score").innerText = playerScore; | |
| initLevel(); | |
| gameWon = false; | |
| gameOver = false; | |
| } | |
| let lastTime = 0; | |
| function gameLoop(currentTime) { | |
| requestAnimationFrame(gameLoop); | |
| const deltaTime = currentTime - lastTime; | |
| lastTime = currentTime; | |
| if (gamePaused) { | |
| return; | |
| } | |
| // Обновление таймера щита | |
| if (shieldActive) { | |
| shieldTimer -= deltaTime / 1000; // Преобразуем миллисекунды в секунды | |
| if (shieldTimer <= 0) { | |
| shieldActive = false; | |
| shieldTimer = 0; | |
| console.log("Действие щита закончилось!"); | |
| } | |
| } | |
| ctx.clearRect(0, 0, canvasWidth, canvasHeight); | |
| drawMaze(); | |
| drawSpy(); | |
| drawGuards(); | |
| moveGuards(deltaTime); | |
| checkCollisions(); | |
| if (gameOver) { | |
| gameOver = true; | |
| gamePaused = true; | |
| } | |
| if (gameWon) { | |
| drawWin(); | |
| return; | |
| } | |
| if (!gamePaused) { | |
| elapsedTime = (Date.now() - startTime) / 1000; | |
| document.getElementById("time").innerText = formatTime(elapsedTime); | |
| } | |
| } | |
| function generateExit() { | |
| let exitX, exitY; | |
| let startX = 1; | |
| let startY = 1; | |
| let targetX, targetY; | |
| do { | |
| targetX = Math.floor(Math.random() * (mazeWidth - 2)) + 1; | |
| targetY = Math.floor(Math.random() * (mazeHeight - 2)) + 1; | |
| } while (maze[targetY]?.[targetX] === 1 || (targetX === startX && targetY === startY)); | |
| const path = aStar(startX, startY, targetX, targetY, maze); | |
| if (path.length > 0) { | |
| exitX = path[path.length - 1].x; | |
| exitY = path[path.length - 1].y; | |
| maze[exitY][exitX] = 2; // 2 означает выход | |
| } else { | |
| // Если путь не найден, устанавливаем выход в соседней клетке от старта | |
| if (maze[1][2] === 0) { | |
| exitX = 2; | |
| exitY = 1; | |
| } else { | |
| exitX = 1; | |
| exitY = 2; | |
| } | |
| maze[exitY][exitX] = 2; | |
| } | |
| } | |
| function generatePaperPieces() { | |
| paperPieces = []; | |
| for (let i = 0; i < 5; i++) { | |
| let x, y; | |
| do { | |
| x = Math.floor(Math.random() * mazeWidth); | |
| y = Math.floor(Math.random() * mazeHeight); | |
| } while (maze[y][x] !== 0 || (x === 1 && y === 1) || (x == mazeWidth - 2 && y == mazeHeight - 2) || isNearExistingPiece(x, y)); // Убеждаемся, что кусочек не появляется на стене, на старте, на финише или рядом с другим кусочком | |
| paperPieces.push({ x: x, y: y }); | |
| } | |
| } | |
| function isNearExistingPiece(x, y) { | |
| for (let i = 0; i < paperPieces.length; i++) { | |
| const piece = paperPieces[i]; | |
| const dx = x - piece.x; | |
| const dy = y - piece.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 2) { // Проверяем расстояние между кусочками | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // === 9. Запуск игры === | |
| const hud = document.getElementById("hud") | |
| const controls = document.getElementById("controls") | |
| canvas.style.display = 'none' | |
| hud.style.display = 'none' | |
| controls.style.display = 'none' | |
| document.getElementById("startGameButton").addEventListener("click", () => { | |
| init() | |
| gameLoop() | |
| startScreen.style.display = "none" | |
| controls.style.display = "block" | |
| hud.style.display = "grid" | |
| canvas.style.display = "block" | |
| }) | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment