"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { useTranslations } from "next-intl"; import { ArrowUpIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowClockwiseIcon, PlayIcon, PauseIcon } from "@phosphor-icons/react"; interface Position { x: number; y: number; } type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT"; const GRID_SIZE = 20; const INITIAL_SNAKE: Position[] = [ { x: 10, y: 10 }, { x: 9, y: 10 }, { x: 8, y: 10 }, ]; const INITIAL_DIRECTION: Direction = "RIGHT"; const GAME_SPEED = 150; export const SnakeGame = () => { const t = useTranslations("notFound"); const canvasRef = useRef(null); const containerRef = useRef(null); const [snake, setSnake] = useState(INITIAL_SNAKE); const [direction, setDirection] = useState(INITIAL_DIRECTION); const [food, setFood] = useState({ x: 15, y: 15 }); const [gameOver, setGameOver] = useState(false); const [gameStarted, setGameStarted] = useState(false); const [score, setScore] = useState(0); const [highScore, setHighScore] = useState(0); const [isPaused, setIsPaused] = useState(false); const [colors, setColors] = useState({ snake: "#00ff00", food: "#ff0000", grid: "#333333" }); const [cellSize, setCellSize] = useState(20); const directionRef = useRef(INITIAL_DIRECTION); const gameLoopRef = useRef(null); useEffect(() => { const savedHighScore = localStorage.getItem("snakeHighScore"); if (savedHighScore) { setHighScore(parseInt(savedHighScore)); } const updateColors = () => { const theme = document.documentElement.getAttribute("data-webtui-theme"); if (theme === "catppuccin-mocha") { setColors({ snake: "#9ca0b0", food: "#f38ba8", grid: "#313244", }); } else { setColors({ snake: "#313244", food: "#d20f39", grid: "#9ca0b0", }); } }; updateColors(); const observer = new MutationObserver(updateColors); observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-webtui-theme"], }); const updateCellSize = () => { if (containerRef.current) { const containerWidth = containerRef.current.offsetWidth; const maxCanvasSize = Math.min(containerWidth - 32, 400); const newCellSize = Math.floor(maxCanvasSize / GRID_SIZE); setCellSize(newCellSize); } }; updateCellSize(); window.addEventListener("resize", updateCellSize); return () => { observer.disconnect(); window.removeEventListener("resize", updateCellSize); }; }, []); const generateFood = useCallback((): Position => { let newFood: Position; do { newFood = { x: Math.floor(Math.random() * GRID_SIZE), y: Math.floor(Math.random() * GRID_SIZE), }; } while (snake.some((segment) => segment.x === newFood.x && segment.y === newFood.y)); return newFood; }, [snake]); const resetGame = useCallback(() => { setSnake(INITIAL_SNAKE); setDirection(INITIAL_DIRECTION); directionRef.current = INITIAL_DIRECTION; setFood(generateFood()); setGameOver(false); setGameStarted(true); setScore(0); setIsPaused(false); }, [generateFood]); const draw = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const theme = document.documentElement.getAttribute("data-webtui-theme"); const bgColor = theme === "catppuccin-mocha" ? "#1e1e2e" : "#eff1f5"; ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = colors.grid; ctx.lineWidth = 1; for (let i = 0; i <= GRID_SIZE; i++) { ctx.beginPath(); ctx.moveTo(i * cellSize, 0); ctx.lineTo(i * cellSize, GRID_SIZE * cellSize); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, i * cellSize); ctx.lineTo(GRID_SIZE * cellSize, i * cellSize); ctx.stroke(); } snake.forEach((segment) => { ctx.fillStyle = colors.snake; ctx.fillRect( segment.x * cellSize + 1, segment.y * cellSize + 1, cellSize - 2, cellSize - 2 ); }); ctx.fillStyle = colors.food; ctx.fillRect( food.x * cellSize + 1, food.y * cellSize + 1, cellSize - 2, cellSize - 2 ); }, [snake, food, colors, cellSize]); useEffect(() => { draw(); }, [draw]); const moveSnake = useCallback(() => { if (gameOver || !gameStarted || isPaused) return; setSnake((prevSnake) => { const head = prevSnake[0]; const newHead: Position = { ...head }; switch (directionRef.current) { case "UP": newHead.y -= 1; break; case "DOWN": newHead.y += 1; break; case "LEFT": newHead.x -= 1; break; case "RIGHT": newHead.x += 1; break; } if ( newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE || prevSnake.some((segment) => segment.x === newHead.x && segment.y === newHead.y) ) { setGameOver(true); setGameStarted(false); return prevSnake; } const newSnake = [newHead, ...prevSnake]; if (newHead.x === food.x && newHead.y === food.y) { setScore((prev) => { const newScore = prev + 10; if (newScore > highScore) { setHighScore(newScore); localStorage.setItem("snakeHighScore", newScore.toString()); } return newScore; }); setFood(generateFood()); } else { newSnake.pop(); } return newSnake; }); }, [gameOver, gameStarted, isPaused, food, highScore, generateFood]); useEffect(() => { if (gameStarted && !gameOver && !isPaused) { gameLoopRef.current = setInterval(moveSnake, GAME_SPEED); } return () => { if (gameLoopRef.current) { clearInterval(gameLoopRef.current); } }; }, [gameStarted, gameOver, isPaused, moveSnake]); useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { if (e.code === "Space") { e.preventDefault(); if (gameOver) { resetGame(); } else if (!gameStarted) { setGameStarted(true); } return; } if (e.code === "KeyP") { e.preventDefault(); if (gameStarted && !gameOver) { setIsPaused((prev) => !prev); } return; } if (!gameStarted || gameOver || isPaused) return; let newDirection: Direction | null = null; switch (e.key) { case "ArrowUp": if (directionRef.current !== "DOWN") { newDirection = "UP"; } break; case "ArrowDown": if (directionRef.current !== "UP") { newDirection = "DOWN"; } break; case "ArrowLeft": if (directionRef.current !== "RIGHT") { newDirection = "LEFT"; } break; case "ArrowRight": if (directionRef.current !== "LEFT") { newDirection = "RIGHT"; } break; } if (newDirection) { e.preventDefault(); directionRef.current = newDirection; setDirection(newDirection); } }; window.addEventListener("keydown", handleKeyPress); return () => window.removeEventListener("keydown", handleKeyPress); }, [gameOver, gameStarted, isPaused, resetGame]); const handleTouchMove = (dir: Direction) => { if (!gameStarted) { setGameStarted(true); directionRef.current = dir; setDirection(dir); return; } if (gameOver || isPaused) return; let canMove = false; switch (dir) { case "UP": canMove = directionRef.current !== "DOWN"; break; case "DOWN": canMove = directionRef.current !== "UP"; break; case "LEFT": canMove = directionRef.current !== "RIGHT"; break; case "RIGHT": canMove = directionRef.current !== "LEFT"; break; } if (canMove) { directionRef.current = dir; setDirection(dir); } }; const handleCanvasClick = () => { if (gameOver) { resetGame(); } else if (!gameStarted) { setGameStarted(true); } }; return (
{t("score")}: {score}
{t("highScore")}: {highScore}
{!gameStarted && !gameOver && (

{t("pressToStart")}

{t("pauseGame")}

)} {gameOver && (

{t("gameOver")}

{t("score")}: {score}

{t("pressToRestart")}

)} {isPaused && (

{t("paused")}

)}

{t("useArrowKeys")}

{t("tapToMove")}

); };