import { useCallback, useEffect, useRef, useState } from 'react'; import './App.css'; const CELL_SIZE = 20; const DEFAULT_CANVAS_SIZE = 400; // Default canvas size for initial state const SPECIAL_EVERY = 5; const SPECIAL_BONUS = 5; const SPECIAL_DURATION_MS = 5000; function getSpeedForLevel(level) { const clamped = Math.min(5, Math.max(1, level)); return 170 - (clamped - 1) * 25; } export default function App() { const canvasRef = useRef(null); const containerRef = useRef(null); const gameLoopRef = useRef(null); const snakeRef = useRef([]); const foodRef = useRef({ x: 0, y: 0 }); const specialFoodRef = useRef(null); const specialExpiresAtRef = useRef(0); const directionRef = useRef({ dx: 1, dy: 0 }); const foodsEatenRef = useRef(0); const scoreRef = useRef(0); const lastProcessedDirectionRef = useRef({ dx: 1, dy: 0 }); const isPausedRef = useRef(false); const [canvasSize, setCanvasSize] = useState(DEFAULT_CANVAS_SIZE); const [gridWidth, setGridWidth] = useState(Math.floor(DEFAULT_CANVAS_SIZE / CELL_SIZE)); const [gridHeight, setGridHeight] = useState(Math.floor(DEFAULT_CANVAS_SIZE / CELL_SIZE)); const [score, setScore] = useState(0); const [isPaused, setIsPaused] = useState(false); const [highScore, setHighScore] = useState(() => { return Number(localStorage.getItem('snakeHighList')) || 0; }); const [level, setLevel] = useState('1'); const [startTitle, setStartTitle] = useState('Snake Game'); const [finalScore, setFinalScore] = useState(0); const [showStartScreen, setShowStartScreen] = useState(true); const [scoreUpdated, setScoreUpdated] = useState(false); const [gameOverShake, setGameOverShake] = useState(false); const [coinDropping, setCoinDropping] = useState(false); // Touch control refs const touchStartRef = useRef({ x: 0, y: 0 }); const touchStartTimeRef = useRef(0); const placeFood = useCallback(() => { let candidate; do { candidate = { x: Math.floor(Math.random() * gridWidth), y: Math.floor(Math.random() * gridHeight), }; } while ( snakeRef.current.some( (segment) => segment.x === candidate.x && segment.y === candidate.y ) || (specialFoodRef.current && specialFoodRef.current.x === candidate.x && specialFoodRef.current.y === candidate.y) ); return candidate; }, [gridWidth, gridHeight]); const placeSpecialFood = useCallback(() => { let candidate; do { candidate = { x: Math.floor(Math.random() * gridWidth), y: Math.floor(Math.random() * gridHeight), }; } while ( snakeRef.current.some( (segment) => segment.x === candidate.x && segment.y === candidate.y ) || (foodRef.current && foodRef.current.x === candidate.x && foodRef.current.y === candidate.y) ); return candidate; }, [gridWidth, gridHeight]); // CRT-style background with subtle grid const drawBackground = useCallback((ctx) => { // Dark CRT background ctx.fillStyle = '#0a0a12'; ctx.fillRect(0, 0, canvasSize, canvasSize); // Subtle grid lines (very faint) ctx.strokeStyle = 'rgba(0, 255, 247, 0.03)'; ctx.lineWidth = 1; for (let i = 0; i <= gridWidth; i++) { ctx.beginPath(); ctx.moveTo(i * CELL_SIZE, 0); ctx.lineTo(i * CELL_SIZE, canvasSize); ctx.stroke(); } for (let i = 0; i <= gridHeight; i++) { ctx.beginPath(); ctx.moveTo(0, i * CELL_SIZE); ctx.lineTo(canvasSize, i * CELL_SIZE); ctx.stroke(); } }, [canvasSize, gridWidth, gridHeight]); // Phosphor green snake with glow const drawSnake = useCallback((ctx) => { const snake = snakeRef.current; const { dx, dy } = directionRef.current; snake.forEach((segment, index) => { const x = segment.x * CELL_SIZE; const y = segment.y * CELL_SIZE; // Glow effect for head if (index === 0) { ctx.shadowColor = '#00ff00'; ctx.shadowBlur = 10; } else { ctx.shadowBlur = 0; } // Phosphor green color - head is brighter ctx.fillStyle = index === 0 ? '#00ff00' : '#00cc00'; ctx.fillRect(x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2); // Reset shadow ctx.shadowBlur = 0; // Draw eyes on head if (index === 0) { ctx.fillStyle = '#000'; let eye1X, eye1Y, eye2X, eye2Y; if (dx === 1) { eye1X = x + 12; eye1Y = y + 5; eye2X = x + 12; eye2Y = y + 12; } else if (dx === -1) { eye1X = x + 5; eye1Y = y + 5; eye2X = x + 5; eye2Y = y + 12; } else if (dy === 1) { eye1X = x + 5; eye1Y = y + 12; eye2X = x + 12; eye2Y = y + 12; } else { eye1X = x + 5; eye1Y = y + 5; eye2X = x + 12; eye2Y = y + 5; } ctx.fillRect(eye1X, eye1Y, 3, 3); ctx.fillRect(eye2X, eye2Y, 3, 3); } }); }, []); // Food with neon glow const drawFood = useCallback((ctx) => { const food = foodRef.current; const x = food.x * CELL_SIZE + CELL_SIZE / 2; const y = food.y * CELL_SIZE + CELL_SIZE / 2; // Glow effect ctx.shadowColor = '#ff6600'; ctx.shadowBlur = 15; ctx.fillStyle = '#ff6600'; ctx.beginPath(); ctx.arc(x, y, CELL_SIZE / 2 - 2, 0, Math.PI * 2); ctx.fill(); // Reset shadow ctx.shadowBlur = 0; }, []); // Special food with cyan glow const drawSpecialFood = useCallback((ctx) => { const specialFood = specialFoodRef.current; if (!specialFood) return; const x = specialFood.x * CELL_SIZE + CELL_SIZE / 2; const y = specialFood.y * CELL_SIZE + CELL_SIZE / 2; // Pulsing glow effect const pulse = Math.sin(Date.now() / 100) * 0.3 + 0.7; ctx.shadowColor = '#00fff7'; ctx.shadowBlur = 20 * pulse; ctx.fillStyle = '#00fff7'; ctx.beginPath(); ctx.arc(x, y, CELL_SIZE / 2 - 2, 0, Math.PI * 2); ctx.fill(); // Reset shadow ctx.shadowBlur = 0; }, []); const drawFrame = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; drawBackground(ctx); drawFood(ctx); drawSpecialFood(ctx); drawSnake(ctx); }, [drawBackground, drawFood, drawSnake, drawSpecialFood]); const resetGame = useCallback(() => { const startX = Math.floor(gridWidth / 2); const startY = Math.floor(gridHeight / 2); snakeRef.current = [ { x: startX, y: startY }, { x: startX - 1, y: startY }, { x: startX - 2, y: startY }, ]; directionRef.current = { dx: 1, dy: 0 }; lastProcessedDirectionRef.current = { dx: 1, dy: 0 }; scoreRef.current = 0; foodsEatenRef.current = 0; specialFoodRef.current = null; specialExpiresAtRef.current = 0; setScore(0); setScoreUpdated(false); foodRef.current = placeFood(); drawFrame(); }, [drawFrame, placeFood, gridWidth, gridHeight]); const endGame = useCallback(() => { if (gameLoopRef.current !== null) { clearInterval(gameLoopRef.current); gameLoopRef.current = null; } setGameOverShake(true); setTimeout(() => setGameOverShake(false), 500); setStartTitle('Game Over'); setFinalScore(scoreRef.current); if (scoreRef.current > highScore) { setHighScore(scoreRef.current); localStorage.setItem('snakeHighList', scoreRef.current); } setShowStartScreen(true); }, [highScore]); const update = useCallback(() => { if (isPausedRef.current) return; const snake = snakeRef.current; const { dx, dy } = directionRef.current; if (snake.length === 0) return; lastProcessedDirectionRef.current = directionRef.current; const head = { x: snake[0].x + dx, y: snake[0].y + dy }; if ( specialFoodRef.current && Date.now() > specialExpiresAtRef.current ) { specialFoodRef.current = null; specialExpiresAtRef.current = 0; } if (head.x < 0 || head.x >= gridWidth || head.y < 0 || head.y >= gridHeight) { endGame(); return; } if (snake.some((segment) => segment.x === head.x && segment.y === head.y)) { endGame(); return; } snake.unshift(head); if (head.x === foodRef.current.x && head.y === foodRef.current.y) { scoreRef.current += 1; foodsEatenRef.current += 1; setScore(scoreRef.current); setScoreUpdated(true); setTimeout(() => setScoreUpdated(false), 300); foodRef.current = placeFood(); if (foodsEatenRef.current % SPECIAL_EVERY === 0) { specialFoodRef.current = placeSpecialFood(); specialExpiresAtRef.current = Date.now() + SPECIAL_DURATION_MS; } } else { snake.pop(); } if ( specialFoodRef.current && head.x === specialFoodRef.current.x && head.y === specialFoodRef.current.y ) { scoreRef.current += SPECIAL_BONUS; setScore(scoreRef.current); setScoreUpdated(true); setTimeout(() => setScoreUpdated(false), 300); specialFoodRef.current = null; specialExpiresAtRef.current = 0; } drawFrame(); }, [drawFrame, endGame, placeFood, placeSpecialFood, gridWidth, gridHeight]); const startGame = useCallback(() => { if (gameLoopRef.current !== null) { clearInterval(gameLoopRef.current); } setStartTitle('Snake Game'); setFinalScore(0); resetGame(); isPausedRef.current = false; setIsPaused(false); const speed = getSpeedForLevel(Number(level)); gameLoopRef.current = setInterval(update, speed); setShowStartScreen(false); }, [level, resetGame, update]); // Coin slot animation const handleCoinClick = useCallback(() => { if (coinDropping) return; setCoinDropping(true); setTimeout(() => setCoinDropping(false), 600); }, [coinDropping]); // Handle canvas resize based on container useEffect(() => { const updateCanvasSize = () => { if (containerRef.current) { const containerWidth = containerRef.current.clientWidth; const containerHeight = containerRef.current.clientHeight; // Use the smaller dimension to maintain square aspect ratio // but maximize the use of available space const size = Math.min(containerWidth, containerHeight); // Round down to nearest multiple of CELL_SIZE for clean grid const roundedSize = Math.floor(size / CELL_SIZE) * CELL_SIZE; // Ensure minimum size (10 cells) const newSize = Math.max(roundedSize, CELL_SIZE * 10); setCanvasSize(prevSize => { if (prevSize !== newSize) { // Grid dimensions changed - reset game state to prevent out-of-bounds issues const newGridWidth = Math.floor(newSize / CELL_SIZE); const newGridHeight = Math.floor(newSize / CELL_SIZE); // Reposition snake to center of new grid if it's outside bounds const snake = snakeRef.current; if (snake.length > 0) { const maxX = newGridWidth - 1; const maxY = newGridHeight - 1; const head = snake[0]; // Check if snake head is out of bounds if (head.x > maxX || head.y > maxY) { // Reset snake to center of new grid const startX = Math.floor(newGridWidth / 2); const startY = Math.floor(newGridHeight / 2); snakeRef.current = [ { x: startX, y: startY }, { x: startX - 1, y: startY }, { x: startX - 2, y: startY }, ]; directionRef.current = { dx: 1, dy: 0 }; lastProcessedDirectionRef.current = { dx: 1, dy: 0 }; } } // Reposition food if out of bounds if (foodRef.current.x >= newGridWidth || foodRef.current.y >= newGridHeight) { foodRef.current = { x: Math.floor(Math.random() * newGridWidth), y: Math.floor(Math.random() * newGridHeight), }; } // Remove special food if out of bounds if (specialFoodRef.current && (specialFoodRef.current.x >= newGridWidth || specialFoodRef.current.y >= newGridHeight)) { specialFoodRef.current = null; specialExpiresAtRef.current = 0; } setGridWidth(newGridWidth); setGridHeight(newGridHeight); } return newSize; }); } }; // Initial size calculation with a small delay to ensure container is rendered const initialTimer = setTimeout(updateCanvasSize, 100); // Use ResizeObserver for more accurate container size detection const resizeObserver = new ResizeObserver(() => { updateCanvasSize(); }); if (containerRef.current) { resizeObserver.observe(containerRef.current); } // Also update on window resize window.addEventListener('resize', updateCanvasSize); return () => { clearTimeout(initialTimer); resizeObserver.disconnect(); window.removeEventListener('resize', updateCanvasSize); }; }, []); useEffect(() => { resetGame(); return () => { if (gameLoopRef.current !== null) { clearInterval(gameLoopRef.current); } }; }, [resetGame]); useEffect(() => { const onKeyDown = (event) => { const key = event.key.toLowerCase(); const { dx, dy } = lastProcessedDirectionRef.current; switch (key) { case 'arrowup': case 'w': if (dy === 0) { directionRef.current = { dx: 0, dy: -1 }; } event.preventDefault(); break; case 'arrowdown': case 's': if (dy === 0) { directionRef.current = { dx: 0, dy: 1 }; } event.preventDefault(); break; case 'arrowleft': case 'a': if (dx === 0) { directionRef.current = { dx: -1, dy: 0 }; } event.preventDefault(); break; case 'arrowright': case 'd': if (dx === 0) { directionRef.current = { dx: 1, dy: 0 }; } event.preventDefault(); break; case ' ': case 'escape': if (gameLoopRef.current) { isPausedRef.current = !isPausedRef.current; setIsPaused((prev) => !prev); event.preventDefault(); } break; default: break; } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, []); // Touch swipe handling const handleTouchStart = useCallback((e) => { const touch = e.touches[0]; touchStartRef.current = { x: touch.clientX, y: touch.clientY }; touchStartTimeRef.current = Date.now(); }, []); const handleTouchMove = useCallback((e) => { e.preventDefault(); }, []); const handleTouchEnd = useCallback((e) => { const touch = e.changedTouches[0]; const dx = touch.clientX - touchStartRef.current.x; const dy = touch.clientY - touchStartRef.current.y; const touchDuration = Date.now() - touchStartTimeRef.current; const minSwipeDistance = 30; const maxTouchDuration = 300; if (Math.abs(dx) < minSwipeDistance && Math.abs(dy) < minSwipeDistance) return; if (touchDuration > maxTouchDuration) return; const lastDir = lastProcessedDirectionRef.current; if (Math.abs(dx) > Math.abs(dy)) { if (dx > 0 && lastDir.dx === 0) { directionRef.current = { dx: 1, dy: 0 }; } else if (dx < 0 && lastDir.dx === 0) { directionRef.current = { dx: -1, dy: 0 }; } } else { if (dy > 0 && lastDir.dy === 0) { directionRef.current = { dx: 0, dy: 1 }; } else if (dy < 0 && lastDir.dy === 0) { directionRef.current = { dx: 0, dy: -1 }; } } }, []); // Direction button handler for on-screen controls const handleDirectionButton = useCallback((dx, dy) => { const lastDir = lastProcessedDirectionRef.current; if (dx !== 0 && lastDir.dx === 0) { directionRef.current = { dx, dy: 0 }; } else if (dy !== 0 && lastDir.dy === 0) { directionRef.current = { dx: 0, dy }; } }, []); return (
{/* Marquee Area */}

Snake Reactor

Arcade Edition

{/* Decorative Rivets */}
{/* LED Score Display */}
Score {String(score).padStart(4, '0')}
Level {level}
High {String(Math.max(score, highScore)).padStart(4, '0')}
{/* CRT Screen Area */}
{showStartScreen && (

{startTitle}

{startTitle === 'Game Over' && (

Score: {finalScore}

{finalScore >= highScore && finalScore > 0 && (

New High Score!

)}
)}
)} {isPaused && !showStartScreen && (

Paused

)}
{/* Control Panel */}
{/* Coin Slot */}
Insert Coin
{/* Power Switch */}
Power
{/* Volume Knob */}
Volume
{/* LED Indicators */}
{/* Start Button */} {!showStartScreen && (
)} {/* Direction Controls */}
{/* Brand Plaque */}
ARCADE SYSTEMS © 1985
{/* Speaker Grille */}
{[...Array(24)].map((_, i) => (
))}
{/* Cabinet Vents */}
{/* Cabinet Screws */}
); }