import { useState, useEffect, useCallback, useRef } from 'react'; import { motion } from 'framer-motion'; import { useSettings } from '@/contexts/SettingsContext'; import { useAchievements } from '@/contexts/AchievementsContext'; 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'; import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock'; const BOARD_WIDTH = 10; const BOARD_HEIGHT = 20; const TICK_SPEED = 500; const HIGHSCORE_KEY = 'tetris-highscore'; const MAX_SCORE = 4294967296; type Board = (string | null)[][]; const TETROMINOS = { I: { shape: [[1, 1, 1, 1]], color: 'hsl(var(--primary))' }, O: { shape: [[1, 1], [1, 1]], color: 'hsl(var(--primary))' }, T: { shape: [[0, 1, 0], [1, 1, 1]], color: 'hsl(var(--primary))' }, S: { shape: [[0, 1, 1], [1, 1, 0]], color: 'hsl(var(--primary))' }, Z: { shape: [[1, 1, 0], [0, 1, 1]], color: 'hsl(var(--primary))' }, J: { shape: [[1, 0, 0], [1, 1, 1]], color: 'hsl(var(--primary))' }, L: { shape: [[0, 0, 1], [1, 1, 1]], color: 'hsl(var(--primary))' }, }; type TetrominoKey = keyof typeof TETROMINOS; interface Piece { shape: number[][]; color: string; x: number; y: number; } const createBoard = (): Board => Array.from({ length: BOARD_HEIGHT }, () => Array(BOARD_WIDTH).fill(null)); const randomTetromino = (): Piece => { const keys = Object.keys(TETROMINOS) as TetrominoKey[]; const key = keys[Math.floor(Math.random() * keys.length)]; const tetromino = TETROMINOS[key]; return { shape: tetromino.shape, color: tetromino.color, x: Math.floor(BOARD_WIDTH / 2) - Math.floor(tetromino.shape[0].length / 2), y: 0 }; }; const rotate = (matrix: number[][]): number[][] => { const rows = matrix.length; const cols = matrix[0].length; const result: number[][] = []; for (let col = 0; col < cols; col++) { const newRow: number[] = []; for (let row = rows - 1; row >= 0; row--) newRow.push(matrix[row][col]); result.push(newRow); } return result; }; const Tetris = () => { const { playSound } = useSettings(); const { checkGameScoreAchievements, unlockMaxScore } = useAchievements(); const [board, setBoard] = useState(createBoard); const [piece, setPiece] = useState(randomTetromino); const [nextPiece, setNextPiece] = useState(randomTetromino); const [score, setScore] = useState(0); const [highScore, setHighScore] = useState(0); const [lines, setLines] = useState(0); const [gameOver, setGameOver] = useState(false); const [gameComplete, setGameComplete] = useState(false); const [isPaused, setIsPaused] = useState(false); const [gameStarted, setGameStarted] = useState(false); const [showGlitchCrash, setShowGlitchCrash] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const gameRef = useRef(null); const { enterFullscreen, exitFullscreen } = useBrowserFullscreen(); const getCellSize = useCallback(() => { if (typeof window === 'undefined') return 24; const isMobile = window.innerWidth < 768; if (isMobile) { const maxWidth = window.innerWidth - 40; // Reserve space for header + fixed controls dock on mobile const maxHeight = window.innerHeight - 440; return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 22); } return isFullscreen ? 30 : 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]); 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)); }, []); // Check score achievements useEffect(() => { if (score > 0) { checkGameScoreAchievements('tetris', score); if (score >= MAX_SCORE) unlockMaxScore(); } }, [score, checkGameScoreAchievements, unlockMaxScore]); const isValidMove = useCallback((newPiece: Piece, currentBoard: Board): boolean => { for (let y = 0; y < newPiece.shape.length; y++) { for (let x = 0; x < newPiece.shape[y].length; x++) { if (newPiece.shape[y][x]) { const newX = newPiece.x + x; const newY = newPiece.y + y; if (newX < 0 || newX >= BOARD_WIDTH || newY >= BOARD_HEIGHT) return false; if (newY >= 0 && currentBoard[newY][newX]) return false; } } } return true; }, []); const mergePiece = useCallback((currentBoard: Board, currentPiece: Piece): Board => { const newBoard = currentBoard.map(row => [...row]); for (let y = 0; y < currentPiece.shape.length; y++) { for (let x = 0; x < currentPiece.shape[y].length; x++) { if (currentPiece.shape[y][x]) { const boardY = currentPiece.y + y; const boardX = currentPiece.x + x; if (boardY >= 0) newBoard[boardY][boardX] = currentPiece.color; } } } return newBoard; }, []); const clearLines = useCallback((currentBoard: Board): { board: Board; cleared: number } => { const newBoard = currentBoard.filter(row => row.some(cell => !cell)); const cleared = BOARD_HEIGHT - newBoard.length; while (newBoard.length < BOARD_HEIGHT) newBoard.unshift(Array(BOARD_WIDTH).fill(null)); return { board: newBoard, cleared }; }, []); const moveDown = useCallback(() => { if (gameOver || gameComplete || isPaused || !gameStarted) return; const newPiece = { ...piece, y: piece.y + 1 }; if (isValidMove(newPiece, board)) { setPiece(newPiece); } else { const mergedBoard = mergePiece(board, piece); const { board: clearedBoard, cleared } = clearLines(mergedBoard); if (cleared > 0) { playSound('success'); setLines(prev => prev + cleared); setScore(prev => { const newScore = Math.min(prev + cleared * 100 * cleared, MAX_SCORE); if (newScore >= MAX_SCORE) setShowGlitchCrash(true); if (newScore > highScore) { setHighScore(newScore); localStorage.setItem(HIGHSCORE_KEY, newScore.toString()); } return newScore; }); } else { playSound('click'); } setBoard(clearedBoard); const newTetromino = nextPiece; setNextPiece(randomTetromino()); if (!isValidMove(newTetromino, clearedBoard)) { setGameOver(true); playSound('error'); } else { setPiece(newTetromino); } } }, [piece, board, gameOver, gameComplete, isPaused, gameStarted, isValidMove, mergePiece, clearLines, playSound, highScore, nextPiece]); const moveLeft = useCallback(() => { if (gameOver || gameComplete || isPaused || !gameStarted) return; const newPiece = { ...piece, x: piece.x - 1 }; if (isValidMove(newPiece, board)) { setPiece(newPiece); playSound('hover'); } }, [piece, board, gameOver, gameComplete, isPaused, gameStarted, isValidMove, playSound]); const moveRight = useCallback(() => { if (gameOver || gameComplete || isPaused || !gameStarted) return; const newPiece = { ...piece, x: piece.x + 1 }; if (isValidMove(newPiece, board)) { setPiece(newPiece); playSound('hover'); } }, [piece, board, gameOver, gameComplete, isPaused, gameStarted, isValidMove, playSound]); const rotatePiece = useCallback(() => { if (gameOver || gameComplete || isPaused || !gameStarted) return; const rotatedShape = rotate(piece.shape); const newPiece = { ...piece, shape: rotatedShape }; if (isValidMove(newPiece, board)) { setPiece(newPiece); playSound('hover'); } }, [piece, board, gameOver, gameComplete, isPaused, gameStarted, isValidMove, playSound]); const hardDrop = useCallback(() => { if (gameOver || gameComplete || isPaused || !gameStarted) return; let newPiece = { ...piece }; while (isValidMove({ ...newPiece, y: newPiece.y + 1 }, board)) newPiece.y++; setPiece(newPiece); playSound('click'); }, [piece, board, gameOver, gameComplete, isPaused, gameStarted, isValidMove, playSound]); const startGame = () => { setBoard(createBoard()); setPiece(randomTetromino()); setNextPiece(randomTetromino()); setScore(0); setLines(0); setGameOver(false); setGameComplete(false); setIsPaused(false); setGameStarted(true); playSound('success'); gameRef.current?.focus(); }; const togglePause = () => { if (!gameStarted || gameOver || gameComplete) return; setIsPaused(prev => !prev); playSound('click'); }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!gameStarted) return; switch (e.key) { case 'ArrowLeft': case 'a': e.preventDefault(); moveLeft(); break; case 'ArrowRight': case 'd': e.preventDefault(); moveRight(); break; case 'ArrowDown': case 's': e.preventDefault(); moveDown(); break; case 'ArrowUp': case 'w': e.preventDefault(); rotatePiece(); break; case ' ': e.preventDefault(); hardDrop(); break; case 'p': e.preventDefault(); togglePause(); break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [moveLeft, moveRight, moveDown, rotatePiece, hardDrop, gameStarted]); useEffect(() => { if (!gameStarted || gameOver || gameComplete || isPaused) return; const interval = setInterval(moveDown, TICK_SPEED); return () => clearInterval(interval); }, [moveDown, gameStarted, gameOver, gameComplete, isPaused]); const renderBoard = () => { const displayBoard = board.map(row => [...row]); if (gameStarted && !gameOver && !gameComplete) { for (let y = 0; y < piece.shape.length; y++) { for (let x = 0; x < piece.shape[y].length; x++) { if (piece.shape[y][x]) { const boardY = piece.y + y; const boardX = piece.x + x; if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) displayBoard[boardY][boardX] = piece.color; } } } } return displayBoard.map((row, y) => (
{row.map((cell, x) => (
))}
)); }; if (showGlitchCrash) return window.location.reload()} />; return (
{'<'} Back

Tetris

{renderBoard()}
{!isMobile && (

NEXT

{nextPiece.shape.map((row, y) => (
{row.map((val, x) => (
))}
))}

SCORE

{score.toLocaleString()}

HIGH SCORE

{highScore.toLocaleString()}

max: 4,294,967,296

LINES

{lines}

CONTROLS

← → ↑ ↓ / WASD

Space: Drop

P: Pause

{!gameStarted || gameOver || gameComplete ? ( ) : ( )}
)} {isMobile && ( <>

SCORE

{score.toLocaleString()}

HIGH

{highScore.toLocaleString()}

LINES

{lines}

{gameStarted && !gameOver && !gameComplete ? (
DROP
) : ( )}
)}
{!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && (

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

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

Final Score: {score.toLocaleString()}

Lines: {lines}

)}
)} ); }; export default Tetris;