import { useState, useEffect, useCallback, useRef } from 'react'; import { motion } from 'framer-motion'; import { useSettings } from '@/contexts/SettingsContext'; import { Link } from 'react-router-dom'; import GlitchCrash from '@/components/GlitchCrash'; import { Maximize2, Minimize2 } from 'lucide-react'; import { useBrowserFullscreen } from '@/hooks/useGameDimensions'; import GameTouchButton from '@/components/GameTouchButton'; const GRID_SIZE = 20; const TICK_SPEED = 120; const MAX_SCORE = 4294967296; const HIGHSCORE_KEY = 'snake-highscore'; type Direction = 'up' | 'down' | 'left' | 'right'; type Position = { x: number; y: number }; const Snake = () => { const { playSound } = useSettings(); const [snake, setSnake] = useState([{ x: 10, y: 10 }]); const [direction, setDirection] = useState('right'); const [food, setFood] = useState({ x: 15, y: 10 }); const [score, setScore] = useState(0); const [highScore, setHighScore] = useState(0); const [gameOver, setGameOver] = useState(false); const [gameComplete, setGameComplete] = useState(false); const [gameStarted, setGameStarted] = useState(false); const [isPaused, setIsPaused] = useState(false); const [showGlitchCrash, setShowGlitchCrash] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const gameRef = useRef(null); const directionQueueRef = useRef([]); const currentDirectionRef = useRef('right'); const { enterFullscreen, exitFullscreen } = useBrowserFullscreen(); // Calculate cell size based on screen const getCellSize = useCallback(() => { if (typeof window === 'undefined') return 24; const isMobile = window.innerWidth < 768; if (isMobile) { const maxWidth = window.innerWidth - 32; const maxHeight = window.innerHeight - 260; return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 16); } return isFullscreen ? 28 : 24; }, [isFullscreen]); const [cellSize, setCellSize] = useState(getCellSize); const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; useEffect(() => { const handleResize = () => setCellSize(getCellSize()); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [getCellSize]); useEffect(() => { setCellSize(getCellSize()); }, [isFullscreen, getCellSize]); // Auto-fullscreen on mobile useEffect(() => { if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); } }, [gameStarted, isMobile, isFullscreen, enterFullscreen]); const toggleFullscreen = async () => { if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); } else { setIsFullscreen(false); await exitFullscreen(); } playSound('click'); }; useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape' && isFullscreen) { setIsFullscreen(false); exitFullscreen(); } }; window.addEventListener('keydown', handleEscape); return () => window.removeEventListener('keydown', handleEscape); }, [isFullscreen, exitFullscreen]); useEffect(() => { const saved = localStorage.getItem(HIGHSCORE_KEY); if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE)); }, []); const spawnFood = useCallback((currentSnake: Position[]): Position | null => { const occupied = new Set(currentSnake.map(s => `${s.x},${s.y}`)); const available: Position[] = []; for (let x = 0; x < GRID_SIZE; x++) { for (let y = 0; y < GRID_SIZE; y++) { if (!occupied.has(`${x},${y}`)) available.push({ x, y }); } } if (available.length === 0) return null; return available[Math.floor(Math.random() * available.length)]; }, []); const resetSnakeKeepScore = useCallback(() => { const initialSnake = [{ x: 10, y: 10 }]; setSnake(initialSnake); setDirection('right'); currentDirectionRef.current = 'right'; directionQueueRef.current = []; setFood(spawnFood(initialSnake)!); playSound('success'); }, [spawnFood, playSound]); const startGame = () => { const initialSnake = [{ x: 10, y: 10 }]; setSnake(initialSnake); setDirection('right'); currentDirectionRef.current = 'right'; directionQueueRef.current = []; setFood(spawnFood(initialSnake)!); setScore(0); setGameOver(false); setGameComplete(false); setIsPaused(false); setGameStarted(true); playSound('success'); gameRef.current?.focus(); }; useEffect(() => { if (!gameStarted || gameOver || gameComplete || isPaused) return; const interval = setInterval(() => { const opposite: Record = { up: 'down', down: 'up', left: 'right', right: 'left' }; let nextDir = currentDirectionRef.current; while (directionQueueRef.current.length > 0) { const queuedDir = directionQueueRef.current.shift()!; if (queuedDir !== opposite[currentDirectionRef.current]) { nextDir = queuedDir; break; } } currentDirectionRef.current = nextDir; setDirection(nextDir); setSnake(prev => { const head = prev[0]; let newHead: Position; switch (nextDir) { case 'up': newHead = { x: head.x, y: head.y - 1 }; break; case 'down': newHead = { x: head.x, y: head.y + 1 }; break; case 'left': newHead = { x: head.x - 1, y: head.y }; break; case 'right': newHead = { x: head.x + 1, y: head.y }; break; } if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE) { setGameOver(true); playSound('error'); return prev; } if (prev.some(s => s.x === newHead.x && s.y === newHead.y)) { setGameOver(true); playSound('error'); return prev; } const newSnake = [newHead, ...prev]; if (newHead.x === food.x && newHead.y === food.y) { playSound('success'); setScore(s => { const newScore = Math.min(s + 10, MAX_SCORE); if (newScore >= MAX_SCORE) setShowGlitchCrash(true); if (newScore > highScore) { setHighScore(newScore); localStorage.setItem(HIGHSCORE_KEY, newScore.toString()); } return newScore; }); const newFood = spawnFood(newSnake); if (newFood === null) setTimeout(() => resetSnakeKeepScore(), 500); else setFood(newFood); return newSnake; } newSnake.pop(); return newSnake; }); }, TICK_SPEED); return () => clearInterval(interval); }, [gameStarted, gameOver, gameComplete, isPaused, food, highScore, playSound, spawnFood, resetSnakeKeepScore]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!gameStarted) return; let newDir: Direction | null = null; switch (e.key) { case 'ArrowUp': case 'w': e.preventDefault(); newDir = 'up'; break; case 'ArrowDown': case 's': e.preventDefault(); newDir = 'down'; break; case 'ArrowLeft': case 'a': e.preventDefault(); newDir = 'left'; break; case 'ArrowRight': case 'd': e.preventDefault(); newDir = 'right'; break; case 'p': e.preventDefault(); if (!gameOver && !gameComplete) setIsPaused(p => !p); return; } if (newDir && directionQueueRef.current.length < 3) directionQueueRef.current.push(newDir); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [gameStarted, gameOver, gameComplete]); const renderGrid = () => { const cells = []; const snakeSet = new Set(snake.map(s => `${s.x},${s.y}`)); const head = snake[0]; for (let y = 0; y < GRID_SIZE; y++) { for (let x = 0; x < GRID_SIZE; x++) { const isSnake = snakeSet.has(`${x},${y}`); const isHead = head.x === x && head.y === y; const isFood = food.x === x && food.y === y; cells.push(
{isFood &&
}
); } } return cells; }; if (showGlitchCrash) return window.location.reload()} />; return ( {/* Header */}
{'<'} Back

Snake

{/* Main layout */}
{/* Game Grid */}
{renderGrid()}
{/* Side Panel */} {!isMobile && (

SCORE

{score.toLocaleString()}

HIGH SCORE

{highScore.toLocaleString()}

max: 4,294,967,296

LENGTH

{snake.length}

CONTROLS

← → ↑ ↓ / WASD

P: Pause

{!gameStarted || gameOver || gameComplete ? ( ) : ( )}
)} {/* Mobile Controls */} {isMobile && (

SCORE

{score.toLocaleString()}

HIGH

{highScore.toLocaleString()}

LEN

{snake.length}

{gameStarted && !gameOver && !gameComplete && (
directionQueueRef.current.push('up')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑
directionQueueRef.current.push('left')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">← directionQueueRef.current.push('right')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→
directionQueueRef.current.push('down')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓
)} {(!gameStarted || gameOver || gameComplete) && ( )}
)}
{/* Desktop Overlay */} {!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && (

{gameComplete ? 'GAME COMPLETE!' : gameOver ? 'GAME OVER' : 'PAUSED'}

{(gameOver || gameComplete) && ( <>

Final Score: {score.toLocaleString()}

Length: {snake.length}

)}
)} ); }; export default Snake;