400 lines
9.9 KiB
HTML
400 lines
9.9 KiB
HTML
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Змейка</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
}
|
|
h1 {
|
|
margin-bottom: 20px;
|
|
font-size: 2.5rem;
|
|
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
|
}
|
|
.game-container {
|
|
position: relative;
|
|
border: 3px solid #00ff88;
|
|
border-radius: 10px;
|
|
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
|
}
|
|
canvas {
|
|
display: block;
|
|
background: #0a0a15;
|
|
border-radius: 7px;
|
|
}
|
|
.score-board {
|
|
display: flex;
|
|
gap: 40px;
|
|
margin-bottom: 20px;
|
|
font-size: 1.3rem;
|
|
}
|
|
.score-item {
|
|
background: rgba(0, 255, 136, 0.1);
|
|
padding: 10px 25px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0, 255, 136, 0.3);
|
|
}
|
|
.score-item span {
|
|
color: #00ff88;
|
|
font-weight: bold;
|
|
}
|
|
.controls {
|
|
margin-top: 25px;
|
|
text-align: center;
|
|
color: #888;
|
|
font-size: 0.95rem;
|
|
}
|
|
.controls p {
|
|
margin: 5px 0;
|
|
}
|
|
.btn {
|
|
background: linear-gradient(135deg, #00ff88 0%, #00cc6a 100%);
|
|
color: #1a1a2e;
|
|
border: none;
|
|
padding: 12px 30px;
|
|
font-size: 1.1rem;
|
|
font-weight: bold;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
margin-top: 15px;
|
|
transition: transform 0.1s, box-shadow 0.1s;
|
|
}
|
|
.btn:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 0 15px rgba(0, 255, 136, 0.5);
|
|
}
|
|
.btn:active {
|
|
transform: scale(0.98);
|
|
}
|
|
.game-over {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(0, 0, 0, 0.85);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
border: 2px solid #ff4757;
|
|
display: none;
|
|
}
|
|
.game-over h2 {
|
|
color: #ff4757;
|
|
font-size: 2rem;
|
|
margin-bottom: 15px;
|
|
}
|
|
.game-over p {
|
|
font-size: 1.2rem;
|
|
margin-bottom: 20px;
|
|
}
|
|
.mobile-controls {
|
|
display: none;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
.mobile-btn {
|
|
width: 60px;
|
|
height: 60px;
|
|
background: rgba(0, 255, 136, 0.2);
|
|
border: 2px solid #00ff88;
|
|
border-radius: 50%;
|
|
color: #00ff88;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
.mobile-btn:active {
|
|
background: rgba(0, 255, 136, 0.4);
|
|
}
|
|
@media (max-width: 500px) {
|
|
.mobile-controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 60px);
|
|
justify-items: center;
|
|
}
|
|
.controls {
|
|
display: none;
|
|
}
|
|
canvas {
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🐍 Змейка</h1>
|
|
|
|
<div class="score-board">
|
|
<div class="score-item">Счёт: <span id="score">0</span></div>
|
|
<div class="score-item">Рекорд: <span id="highScore">0</span></div>
|
|
</div>
|
|
|
|
<div class="game-container">
|
|
<canvas id="gameCanvas" width="400" height="400"></canvas>
|
|
<div class="game-over" id="gameOver">
|
|
<h2>Игра окончена!</h2>
|
|
<p>Твой счёт: <span id="finalScore">0</span></p>
|
|
<button class="btn" onclick="restartGame()">Играть снова</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn" id="startBtn" onclick="startGame()">Начать игру</button>
|
|
|
|
<div class="controls">
|
|
<p>Управление: стрелки или WASD</p>
|
|
<p>Пауза: пробел | Перезапуск: Enter</p>
|
|
</div>
|
|
|
|
<div class="mobile-controls">
|
|
<div></div>
|
|
<button class="mobile-btn" onclick="changeDirection('up')">↑</button>
|
|
<div></div>
|
|
<button class="mobile-btn" onclick="changeDirection('left')">←</button>
|
|
<button class="mobile-btn" onclick="changeDirection('down')">↓</button>
|
|
<button class="mobile-btn" onclick="changeDirection('right')">→</button>
|
|
</div>
|
|
|
|
<script>
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const scoreEl = document.getElementById('score');
|
|
const highScoreEl = document.getElementById('highScore');
|
|
const gameOverEl = document.getElementById('gameOver');
|
|
const finalScoreEl = document.getElementById('finalScore');
|
|
const startBtn = document.getElementById('startBtn');
|
|
|
|
const gridSize = 20;
|
|
const tileCount = canvas.width / gridSize;
|
|
|
|
let snake = [];
|
|
let food = {};
|
|
let direction = { x: 1, y: 0 };
|
|
let nextDirection = { x: 1, y: 0 };
|
|
let score = 0;
|
|
let highScore = localStorage.getItem('snakeHighScore') || 0;
|
|
let gameLoop = null;
|
|
let gameSpeed = 100;
|
|
let isGameRunning = false;
|
|
let isPaused = false;
|
|
|
|
highScoreEl.textContent = highScore;
|
|
|
|
function initGame() {
|
|
snake = [
|
|
{ x: 10, y: 10 },
|
|
{ x: 9, y: 10 },
|
|
{ x: 8, y: 10 }
|
|
];
|
|
direction = { x: 1, y: 0 };
|
|
nextDirection = { x: 1, y: 0 };
|
|
score = 0;
|
|
scoreEl.textContent = score;
|
|
placeFood();
|
|
gameOverEl.style.display = 'none';
|
|
startBtn.style.display = 'none';
|
|
}
|
|
|
|
function placeFood() {
|
|
do {
|
|
food = {
|
|
x: Math.floor(Math.random() * tileCount),
|
|
y: Math.floor(Math.random() * tileCount)
|
|
};
|
|
} while (snake.some(seg => seg.x === food.x && seg.y === food.y));
|
|
}
|
|
|
|
function draw() {
|
|
ctx.fillStyle = '#0a0a15';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
for (let i = 0; i < tileCount; i++) {
|
|
for (let j = 0; j < tileCount; j++) {
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
|
|
ctx.strokeRect(i * gridSize, j * gridSize, gridSize, gridSize);
|
|
}
|
|
}
|
|
|
|
ctx.fillStyle = '#ff4757';
|
|
ctx.shadowColor = '#ff4757';
|
|
ctx.shadowBlur = 10;
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
food.x * gridSize + gridSize / 2,
|
|
food.y * gridSize + gridSize / 2,
|
|
gridSize / 2 - 2,
|
|
0,
|
|
Math.PI * 2
|
|
);
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
|
|
snake.forEach((segment, index) => {
|
|
const gradient = ctx.createRadialGradient(
|
|
segment.x * gridSize + gridSize / 2,
|
|
segment.y * gridSize + gridSize / 2,
|
|
0,
|
|
segment.x * gridSize + gridSize / 2,
|
|
segment.y * gridSize + gridSize / 2,
|
|
gridSize / 2
|
|
);
|
|
|
|
if (index === 0) {
|
|
gradient.addColorStop(0, '#00ff88');
|
|
gradient.addColorStop(1, '#00cc6a');
|
|
ctx.shadowColor = '#00ff88';
|
|
ctx.shadowBlur = 10;
|
|
} else {
|
|
gradient.addColorStop(0, '#00cc6a');
|
|
gradient.addColorStop(1, '#009950');
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(
|
|
segment.x * gridSize + 1,
|
|
segment.y * gridSize + 1,
|
|
gridSize - 2,
|
|
gridSize - 2
|
|
);
|
|
});
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
function update() {
|
|
if (isPaused) return;
|
|
|
|
direction = { ...nextDirection };
|
|
|
|
const head = {
|
|
x: snake[0].x + direction.x,
|
|
y: snake[0].y + direction.y
|
|
};
|
|
|
|
if (head.x < 0 || head.x >= tileCount ||
|
|
head.y < 0 || head.y >= tileCount ||
|
|
snake.some(seg => seg.x === head.x && seg.y === head.y)) {
|
|
endGame();
|
|
return;
|
|
}
|
|
|
|
snake.unshift(head);
|
|
|
|
if (head.x === food.x && head.y === food.y) {
|
|
score += 10;
|
|
scoreEl.textContent = score;
|
|
if (score > highScore) {
|
|
highScore = score;
|
|
highScoreEl.textContent = highScore;
|
|
localStorage.setItem('snakeHighScore', highScore);
|
|
}
|
|
placeFood();
|
|
if (gameSpeed > 50) {
|
|
gameSpeed -= 2;
|
|
clearInterval(gameLoop);
|
|
gameLoop = setInterval(update, gameSpeed);
|
|
}
|
|
} else {
|
|
snake.pop();
|
|
}
|
|
|
|
draw();
|
|
}
|
|
|
|
function startGame() {
|
|
if (isGameRunning) return;
|
|
initGame();
|
|
isGameRunning = true;
|
|
isPaused = false;
|
|
gameSpeed = 100;
|
|
gameLoop = setInterval(update, gameSpeed);
|
|
draw();
|
|
}
|
|
|
|
function endGame() {
|
|
isGameRunning = false;
|
|
clearInterval(gameLoop);
|
|
finalScoreEl.textContent = score;
|
|
gameOverEl.style.display = 'block';
|
|
}
|
|
|
|
function restartGame() {
|
|
clearInterval(gameLoop);
|
|
startGame();
|
|
}
|
|
|
|
function changeDirection(dir) {
|
|
if (!isGameRunning || isPaused) return;
|
|
|
|
const opposites = {
|
|
up: 'down',
|
|
down: 'up',
|
|
left: 'right',
|
|
right: 'left'
|
|
};
|
|
|
|
const dirMap = {
|
|
up: { x: 0, y: -1 },
|
|
down: { x: 0, y: 1 },
|
|
left: { x: -1, y: 0 },
|
|
right: { x: 1, y: 0 }
|
|
};
|
|
|
|
const currentDir = direction.x === 0 ? (direction.y > 0 ? 'down' : 'up') : (direction.x > 0 ? 'right' : 'left');
|
|
|
|
if (opposites[dir] !== currentDir) {
|
|
nextDirection = dirMap[dir];
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (!isGameRunning && e.code === 'Enter') {
|
|
startGame();
|
|
return;
|
|
}
|
|
|
|
if (e.code === 'Space' && isGameRunning) {
|
|
isPaused = !isPaused;
|
|
return;
|
|
}
|
|
|
|
if (e.code === 'Enter' && !isGameRunning) {
|
|
restartGame();
|
|
return;
|
|
}
|
|
|
|
const keyMap = {
|
|
ArrowUp: 'up',
|
|
ArrowDown: 'down',
|
|
ArrowLeft: 'left',
|
|
ArrowRight: 'right',
|
|
KeyW: 'up',
|
|
KeyS: 'down',
|
|
KeyA: 'left',
|
|
KeyD: 'right'
|
|
};
|
|
|
|
if (keyMap[e.code]) {
|
|
e.preventDefault();
|
|
changeDirection(keyMap[e.code]);
|
|
}
|
|
});
|
|
|
|
draw();
|
|
</script>
|
|
</body>
|
|
</html>
|