Spaces:
Build error
Build error
| import { useState, useEffect, useRef, useCallback } from 'react' | |
| const BOARD_SIZE = 20 | |
| const INITIAL_SNAKE = [{ x: 10, y: 10 }] | |
| const INITIAL_FOOD = { x: 15, y: 15 } | |
| const INITIAL_DIRECTION = { x: 0, y: -1 } | |
| const GAME_SPEED = 150 | |
| const DIRECTIONS = { | |
| UP: { x: 0, y: -1 }, | |
| DOWN: { x: 0, y: 1 }, | |
| LEFT: { x: -1, y: 0 }, | |
| RIGHT: { x: 1, y: 0 } | |
| } | |
| export default function SnakeGame() { | |
| const canvasRef = useRef(null) | |
| const gameLoopRef = useRef(null) | |
| const [snake, setSnake] = useState(INITIAL_SNAKE) | |
| const [food, setFood] = useState(INITIAL_FOOD) | |
| const [direction, setDirection] = useState(INITIAL_DIRECTION) | |
| const [gameOver, setGameOver] = useState(false) | |
| const [score, setScore] = useState(0) | |
| const [isPlaying, setIsPlaying] = useState(false) | |
| const [isPaused, setIsPaused] = useState(false) | |
| // Generate random food position | |
| const generateFood = useCallback((currentSnake) => { | |
| let newFood | |
| do { | |
| newFood = { | |
| x: Math.floor(Math.random() * BOARD_SIZE), | |
| y: Math.floor(Math.random() * BOARD_SIZE) | |
| } | |
| } while (currentSnake.some(segment => segment.x === newFood.x && segment.y === newFood.y)) | |
| return newFood | |
| }, []) | |
| // Check collision with walls or self | |
| const checkCollision = useCallback((head, body) => { | |
| // Wall collision | |
| if (head.x < 0 || head.x >= BOARD_SIZE || head.y < 0 || head.y >= BOARD_SIZE) { | |
| return true | |
| } | |
| // Self collision | |
| return body.some(segment => segment.x === head.x && segment.y === head.y) | |
| }, []) | |
| // Move snake | |
| const moveSnake = useCallback(() => { | |
| if (gameOver || isPaused || !isPlaying) return | |
| setSnake(currentSnake => { | |
| const newSnake = [...currentSnake] | |
| const head = { ...newSnake[0] } | |
| head.x += direction.x | |
| head.y += direction.y | |
| // Check collision | |
| if (checkCollision(head, newSnake)) { | |
| setGameOver(true) | |
| setIsPlaying(false) | |
| return currentSnake | |
| } | |
| newSnake.unshift(head) | |
| // Check if food is eaten | |
| if (head.x === food.x && head.y === food.y) { | |
| setScore(prev => prev + 10) | |
| setFood(generateFood(newSnake)) | |
| } else { | |
| newSnake.pop() | |
| } | |
| return newSnake | |
| }) | |
| }, [direction, food, gameOver, isPaused, isPlaying, checkCollision, generateFood]) | |
| // Game loop | |
| useEffect(() => { | |
| if (isPlaying && !gameOver && !isPaused) { | |
| gameLoopRef.current = setInterval(moveSnake, GAME_SPEED) | |
| } else { | |
| clearInterval(gameLoopRef.current) | |
| } | |
| return () => clearInterval(gameLoopRef.current) | |
| }, [isPlaying, gameOver, isPaused, moveSnake]) | |
| // Draw game | |
| const draw = useCallback(() => { | |
| const canvas = canvasRef.current | |
| if (!canvas) return | |
| const ctx = canvas.getContext('2d') | |
| const cellSize = canvas.width / BOARD_SIZE | |
| // Clear canvas | |
| ctx.fillStyle = '#000' | |
| ctx.fillRect(0, 0, canvas.width, canvas.height) | |
| // Draw snake | |
| ctx.fillStyle = '#4ade80' | |
| snake.forEach((segment, index) => { | |
| if (index === 0) { | |
| // Head | |
| ctx.fillStyle = '#22c55e' | |
| } else { | |
| ctx.fillStyle = '#4ade80' | |
| } | |
| ctx.fillRect( | |
| segment.x * cellSize, | |
| segment.y * cellSize, | |
| cellSize - 2, | |
| cellSize - 2 | |
| ) | |
| }) | |
| // Draw food | |
| ctx.fillStyle = '#ef4444' | |
| ctx.fillRect( | |
| food.x * cellSize, | |
| food.y * cellSize, | |
| cellSize - 2, | |
| cellSize - 2 | |
| ) | |
| // Draw grid | |
| ctx.strokeStyle = '#333' | |
| ctx.lineWidth = 1 | |
| for (let i = 0; i <= BOARD_SIZE; i++) { | |
| ctx.beginPath() | |
| ctx.moveTo(i * cellSize, 0) | |
| ctx.lineTo(i * cellSize, canvas.height) | |
| ctx.stroke() | |
| ctx.beginPath() | |
| ctx.moveTo(0, i * cellSize) | |
| ctx.lineTo(canvas.width, i * cellSize) | |
| ctx.stroke() | |
| } | |
| }, [snake, food]) | |
| // Redraw when snake or food changes | |
| useEffect(() => { | |
| draw() | |
| }, [draw]) | |
| // Handle keyboard input | |
| const handleKeyPress = useCallback((e) => { | |
| if (!isPlaying) return | |
| const { key } = e | |
| const currentDirection = direction | |
| switch (key) { | |
| case 'ArrowUp': | |
| case 'w': | |
| case 'W': | |
| if (currentDirection.y !== 1) { | |
| setDirection(DIRECTIONS.UP) | |
| } | |
| break | |
| case 'ArrowDown': | |
| case 's': | |
| case 'S': | |
| if (currentDirection.y !== -1) { | |
| setDirection(DIRECTIONS.DOWN) | |
| } | |
| break | |
| case 'ArrowLeft': | |
| case 'a': | |
| case 'A': | |
| if (currentDirection.x !== 1) { | |
| setDirection(DIRECTIONS.LEFT) | |
| } | |
| break | |
| case 'ArrowRight': | |
| case 'd': | |
| case 'D': | |
| if (currentDirection.x !== -1) { | |
| setDirection(DIRECTIONS.RIGHT) | |
| } | |
| break | |
| case ' ': | |
| e.preventDefault() | |
| setIsPaused(prev => !prev) | |
| break | |
| } | |
| }, [isPlaying, direction]) | |
| useEffect(() => { | |
| window.addEventListener('keydown', handleKeyPress) | |
| return () => window.removeEventListener('keydown', handleKeyPress) | |
| }, [handleKeyPress]) | |
| // Touch controls | |
| const handleDirection = useCallback((newDirection) => { | |
| if (!isPlaying || isPaused) return | |
| const currentDirection = direction | |
| // Prevent reverse direction | |
| if ( | |
| (newDirection === DIRECTIONS.UP && currentDirection.y === 1) || | |
| (newDirection === DIRECTIONS.DOWN && currentDirection.y === -1) || | |
| (newDirection === DIRECTIONS.LEFT && currentDirection.x === 1) || | |
| (newDirection === DIRECTIONS.RIGHT && currentDirection.x === -1) | |
| ) { | |
| return | |
| } | |
| setDirection(newDirection) | |
| }, [isPlaying, isPaused, direction]) | |
| // Start new game | |
| const startNewGame = () => { | |
| setSnake(INITIAL_SNAKE) | |
| setFood(generateFood(INITIAL_SNAKE)) | |
| setDirection(INITIAL_DIRECTION) | |
| setGameOver(false) | |
| setScore(0) | |
| setIsPlaying(true) | |
| setIsPaused(false) | |
| } | |
| return ( | |
| <div className="flex flex-col items-center space-y-6"> | |
| {/* Score */} | |
| <div className="text-center"> | |
| <div className="score-display"> | |
| Score: {score} | |
| </div> | |
| <div className="text-gray-400"> | |
| Length: {snake.length} | |
| </div> | |
| </div> | |
| {/* Game Canvas */} | |
| <div className="relative"> | |
| <canvas | |
| ref={canvasRef} | |
| width={400} | |
| height={400} | |
| className="block bg-gray-900" | |
| /> | |
| {/* Game Over Overlay */} | |
| {gameOver && ( | |
| <div className="absolute inset-0 bg-black bg-opacity-80 flex items-center justify-center rounded-lg"> | |
| <div className="text-center text-white"> | |
| <h2 className="text-3xl font-bold mb-4">Game Over!</h2> | |
| <p className="text-xl mb-4">Final Score: {score}</p> | |
| <button | |
| onClick={startNewGame} | |
| className="game-button" | |
| > | |
| Play Again | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Pause Overlay */} | |
| {isPaused && !gameOver && ( | |
| <div className="absolute inset-0 bg-black bg-opacity-60 flex items-center justify-center rounded-lg"> | |
| <div className="text-center text-white"> | |
| <h2 className="text-3xl font-bold mb-4">Paused</h2> | |
| <p className="text-lg">Press SPACE or tap Resume to continue</p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Controls */} | |
| <div className="flex flex-col items-center space-y-4"> | |
| {!isPlaying || gameOver ? ( | |
| <button | |
| onClick={startNewGame} | |
| className="game-button text-lg px-8 py-4" | |
| > | |
| {gameOver ? 'Play Again' : 'Start Game'} | |
| </button> | |
| ) : ( | |
| <div className="flex space-x-4"> | |
| <button | |
| onClick={() => setIsPaused(!isPaused)} | |
| className="game-button" | |
| > | |
| {isPaused ? 'Resume' : 'Pause'} | |
| </button> | |
| <button | |
| onClick={startNewGame} | |
| className="game-button bg-red-600 hover:bg-red-700" | |
| > | |
| Restart | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| {/* Mobile Touch Controls */} | |
| <div className="md:hidden"> | |
| <div className="text-center text-white mb-4">Touch Controls</div> | |
| <div className="grid grid-cols-3 gap-2 w-48"> | |
| <div></div> | |
| <button | |
| onClick={() => handleDirection(DIRECTIONS.UP)} | |
| className="control-button text-2xl h-16" | |
| disabled={!isPlaying || isPaused} | |
| > | |
| β | |
| </button> | |
| <div></div> | |
| <button | |
| onClick={() => handleDirection(DIRECTIONS.LEFT)} | |
| className="control-button text-2xl h-16" | |
| disabled={!isPlaying || isPaused} | |
| > | |
| β | |
| </button> | |
| <div></div> | |
| <button | |
| onClick={() => handleDirection(DIRECTIONS.RIGHT)} | |
| className="control-button text-2xl h-16" | |
| disabled={!isPlaying || isPaused} | |
| > | |
| β | |
| </button> | |
| <div></div> | |
| <button | |
| onClick={() => handleDirection(DIRECTIONS.DOWN)} | |
| className="control-button text-2xl h-16" | |
| disabled={!isPlaying || isPaused} | |
| > | |
| β | |
| </button> | |
| <div></div> | |
| </div> | |
| </div> | |
| {/* Instructions */} | |
| <div className="text-center text-gray-400 text-sm max-w-md"> | |
| <p className="mb-2"> | |
| <strong>Keyboard:</strong> Arrow keys or WASD to move, SPACE to pause | |
| </p> | |
| <p> | |
| <strong>Mobile:</strong> Use touch controls below | |
| </p> | |
| </div> | |
| </div> | |
| ) | |
| } |