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 { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock'; import { useSwipeControls } from '@/hooks/useSwipeControls'; import GameTouchButton from '@/components/GameTouchButton'; const GRID_WIDTH = 21; const GRID_HEIGHT = 21; const TICK_SPEED = 180; const POWER_DURATION = 8000; const MAX_SCORE = 4294967296; const HIGHSCORE_KEY = 'pacman-highscore'; type Direction = 'up' | 'down' | 'left' | 'right'; type Position = { x: number; y: number }; const MAZE_TEMPLATE = [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,3,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,3,1], [1,0,1,1,0,1,1,1,1,0,1,0,1,1,1,1,0,1,1,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,1,1,0,1,0,1,1,1,1,1,1,1,0,1,0,1,1,0,1], [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1], [1,1,1,1,0,1,1,1,1,0,1,0,1,1,1,1,0,1,1,1,1], [1,1,1,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,1,1,1], [1,1,1,1,0,1,0,1,1,0,0,0,1,1,0,1,0,1,1,1,1], [2,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,2], [1,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1,0,1,1,1,1], [1,1,1,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,1,1,1], [1,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1,0,1,1,1,1], [1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1], [1,0,1,1,0,1,1,1,1,0,1,0,1,1,1,1,0,1,1,0,1], [1,3,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,3,1], [1,1,0,1,0,1,0,1,1,1,1,1,1,1,0,1,0,1,0,1,1], [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1], [1,0,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], ]; const Pacman = () => { const { playSound, mobileControlMode } = useSettings(); const { checkGameScoreAchievements, unlockMaxScore } = useAchievements(); // Game mode selection const [gameMode, setGameMode] = useState<'1p' | '2p'>('1p'); const [showModeSelection, setShowModeSelection] = useState(true); // Player 1 (always active) const [pacman, setPacman] = useState({ x: 10, y: 15 }); const [direction, setDirection] = useState('right'); const [nextDirection, setNextDirection] = useState('right'); const [mouthOpen, setMouthOpen] = useState(true); const [lives, setLives] = useState(3); // Player 2 (for 2P mode) const [pacman2, setPacman2] = useState({ x: 10, y: 15 }); const [direction2, setDirection2] = useState('right'); const [nextDirection2, setNextDirection2] = useState('right'); const [mouthOpen2, setMouthOpen2] = useState(true); const [lives2, setLives2] = useState(3); const [player1Eliminated, setPlayer1Eliminated] = useState(false); const [player2Eliminated, setPlayer2Eliminated] = useState(false); const [showEliminationPrompt, setShowEliminationPrompt] = useState(false); const [ghosts, setGhosts] = useState<{ pos: Position; dir: Direction; eaten: boolean }[]>([ { pos: { x: 9, y: 9 }, dir: 'left', eaten: false }, { pos: { x: 10, y: 9 }, dir: 'up', eaten: false }, { pos: { x: 11, y: 9 }, dir: 'right', eaten: false }, ]); const [dots, setDots] = useState>(new Set()); const [powerPellets, setPowerPellets] = useState>(new Set()); const [isPowered, setIsPowered] = useState(false); const [score, setScore] = useState(0); const [score2, setScore2] = 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 [level, setLevel] = useState(1); const [isFullscreen, setIsFullscreen] = useState(false); const gameRef = useRef(null); const powerTimerRef = 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 - 420; return Math.min(Math.floor(maxWidth / GRID_WIDTH), Math.floor(maxHeight / GRID_HEIGHT), 18); } return isFullscreen ? 30 : 24; }, [isFullscreen]); const [cellSize, setCellSize] = useState(getCellSize); const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; // Swipe controls for mobile const { getSwipeHandlers } = useSwipeControls({ onSwipe: (dir) => { if (gameMode === '1p' && gameStarted && !gameOver && !gameComplete && !isPaused) { setNextDirection(dir as Direction); } }, onTap: () => { if (gameStarted && !gameOver && !gameComplete) setIsPaused(p => !p); }, enabled: isMobile && gameStarted && !gameOver && !gameComplete && mobileControlMode === 'swipe', }); const useButtonControls = isMobile && mobileControlMode === 'buttons'; 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('pacman', score); if (score >= MAX_SCORE) unlockMaxScore(); } }, [score, checkGameScoreAchievements, unlockMaxScore]); const initDots = useCallback(() => { const newDots = new Set(); const newPowerPellets = new Set(); for (let y = 0; y < GRID_HEIGHT; y++) { for (let x = 0; x < GRID_WIDTH; x++) { if (MAZE_TEMPLATE[y][x] === 0) newDots.add(`${x},${y}`); else if (MAZE_TEMPLATE[y][x] === 3) newPowerPellets.add(`${x},${y}`); } } newDots.delete('10,15'); newDots.delete('9,9'); newDots.delete('10,9'); newDots.delete('11,9'); return { dots: newDots, powerPellets: newPowerPellets }; }, []); const canMove = (pos: Position, dir: Direction): boolean => { let newX = pos.x, newY = pos.y; switch (dir) { case 'up': newY--; break; case 'down': newY++; break; case 'left': newX--; break; case 'right': newX++; break; } if (newX < 0) return MAZE_TEMPLATE[newY]?.[GRID_WIDTH - 1] !== 1; if (newX >= GRID_WIDTH) return MAZE_TEMPLATE[newY]?.[0] !== 1; if (newY < 0 || newY >= GRID_HEIGHT) return false; return MAZE_TEMPLATE[newY][newX] !== 1; }; const moveEntity = (pos: Position, dir: Direction): Position => { let newX = pos.x, newY = pos.y; switch (dir) { case 'up': newY--; break; case 'down': newY++; break; case 'left': newX--; break; case 'right': newX++; break; } if (newX < 0) newX = GRID_WIDTH - 1; if (newX >= GRID_WIDTH) newX = 0; return { x: newX, y: newY }; }; const activatePowerMode = () => { if (powerTimerRef.current) clearTimeout(powerTimerRef.current); setIsPowered(true); powerTimerRef.current = setTimeout(() => { setIsPowered(false); setGhosts(prev => prev.map(g => ({ ...g, eaten: false }))); }, POWER_DURATION); }; const startGame = () => { if (powerTimerRef.current) clearTimeout(powerTimerRef.current); setPacman({ x: 10, y: 15 }); setDirection('right'); setNextDirection('right'); setMouthOpen(true); setPacman2({ x: 10, y: 15 }); setDirection2('right'); setNextDirection2('right'); setMouthOpen2(true); setLives(3); setLives2(3); setGhosts([{ pos: { x: 9, y: 9 }, dir: 'left', eaten: false }, { pos: { x: 10, y: 9 }, dir: 'up', eaten: false }, { pos: { x: 11, y: 9 }, dir: 'right', eaten: false }]); const { dots: newDots, powerPellets: newPowerPellets } = initDots(); setDots(newDots); setPowerPellets(newPowerPellets); setIsPowered(false); setScore(0); setScore2(0); setLevel(1); setGameOver(false); setGameComplete(false); setIsPaused(false); setGameStarted(true); setShowModeSelection(false); setPlayer1Eliminated(false); setPlayer2Eliminated(false); setShowEliminationPrompt(false); playSound('success'); gameRef.current?.focus(); }; const continueIn1PMode = () => { setGameMode('1p'); setShowEliminationPrompt(false); setIsPaused(false); playSound('success'); }; useEffect(() => { return () => { if (powerTimerRef.current) clearTimeout(powerTimerRef.current); }; }, []); useEffect(() => { if (!gameStarted || gameOver || gameComplete || isPaused) return; const interval = setInterval(() => { // Handle both players const handlePlayerMovement = ( player: Position, playerDir: Direction, playerNextDir: Direction, setPlayerDir: (dir: Direction) => void, setPlayer: (pos: Position) => void, setPlayerScore: (fn: (prev: number) => number) => void, setPlayerMouth?: (fn: (prev: boolean) => boolean) => void ) => { // Animate mouth every tick for smoother appearance if (setPlayerMouth) setPlayerMouth(prev => !prev); if (canMove(player, playerNextDir)) setPlayerDir(playerNextDir); const actualDir = canMove(player, playerNextDir) ? playerNextDir : playerDir; if (canMove(player, actualDir)) { const newPos = moveEntity(player, actualDir); // Check for ghosts at target position BEFORE moving let ghostAtTarget = null; let ghostIndex = -1; for (let i = 0; i < ghosts.length; i++) { if (ghosts[i].pos.x === newPos.x && ghosts[i].pos.y === newPos.y) { ghostAtTarget = ghosts[i]; ghostIndex = i; break; } } if (ghostAtTarget) { // There's a ghost at the target position if (isPowered && !ghostAtTarget.eaten) { // Eat the ghost - move to position and score setPlayer(newPos); setGhosts(prev => prev.map((g, idx) => idx === ghostIndex ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g)); setPlayerScore(prev => { const ns = Math.min(prev + 200, MAX_SCORE); if (ns >= MAX_SCORE) setShowGlitchCrash(true); if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); } return ns; }); playSound('success'); } else if (!ghostAtTarget.eaten) { // Hit by ghost - lose life without moving const isPlayer1 = player === pacman; const currentLives = isPlayer1 ? lives : lives2; const setLivesFunc = isPlayer1 ? setLives : setLives2; const newLives = currentLives - 1; setLivesFunc(newLives); if (newLives > 0) { // Respawn at start setPlayer({ x: 10, y: 15 }); playSound('error'); } else { // Player eliminated if (isPlayer1) { setPlayer1Eliminated(true); setPacman({ x: -1, y: -1 }); } else { setPlayer2Eliminated(true); setPacman2({ x: -1, y: -1 }); } playSound('error'); if (gameMode === '2p' && ((isPlayer1 && !player2Eliminated) || (!isPlayer1 && !player1Eliminated))) { setShowEliminationPrompt(true); setIsPaused(true); } else { setGameOver(true); } } } } else { // No ghost at target, safe to move setPlayer(newPos); const posKey = `${newPos.x},${newPos.y}`; if (powerPellets.has(posKey)) { setPowerPellets(prev => { const np = new Set(prev); np.delete(posKey); return np; }); activatePowerMode(); setPlayerScore(prev => { const ns = Math.min(prev + 50, MAX_SCORE); if (ns >= MAX_SCORE) setShowGlitchCrash(true); if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); } return ns; }); playSound('success'); } else if (dots.has(posKey)) { setDots(prev => { const nd = new Set(prev); nd.delete(posKey); return nd; }); setPlayerScore(prev => { const ns = Math.min(prev + 10, MAX_SCORE); if (ns >= MAX_SCORE) setShowGlitchCrash(true); if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); } return ns; }); playSound('hover'); } } } }; // Player 1 (only if not eliminated) if (!player1Eliminated) { handlePlayerMovement(pacman, direction, nextDirection, setDirection, setPacman, setScore, setMouthOpen); // Check collisions immediately after movement for (let i = 0; i < ghosts.length; i++) { const ghost = ghosts[i]; if (ghost.pos.x === pacman.x && ghost.pos.y === pacman.y) { if (isPowered && !ghost.eaten) { setGhosts(prev => prev.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g)); setScore(prev => { const ns = Math.min(prev + 200, MAX_SCORE); if (ns >= MAX_SCORE) setShowGlitchCrash(true); if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); } return ns; }); playSound('success'); } else if (!ghost.eaten) { // Player 1 dies const newLives = lives - 1; setLives(newLives); if (newLives > 0) { setPacman({ x: 10, y: 15 }); playSound('error'); } else { setPlayer1Eliminated(true); setPacman({ x: -1, y: -1 }); playSound('error'); if (gameMode === '2p' && !player2Eliminated) { setShowEliminationPrompt(true); setIsPaused(true); } else { setGameOver(true); } } } } } } // Player 2 (only in 2P mode and not eliminated) if (gameMode === '2p' && !player2Eliminated) { handlePlayerMovement(pacman2, direction2, nextDirection2, setDirection2, setPacman2, setScore2, setMouthOpen2); } setGhosts(prev => prev.map(ghost => { if (ghost.eaten) return ghost; const directions: Direction[] = ['up', 'down', 'left', 'right']; const opposite: Record = { up: 'down', down: 'up', left: 'right', right: 'left' }; const validDirs = directions.filter(d => d !== opposite[ghost.dir] && canMove(ghost.pos, d)); if (validDirs.length === 0) { const anyValid = directions.filter(d => canMove(ghost.pos, d)); if (anyValid.length === 0) return ghost; const dir = anyValid[Math.floor(Math.random() * anyValid.length)]; return { ...ghost, pos: moveEntity(ghost.pos, dir), dir }; } const dx = pacman.x - ghost.pos.x, dy = pacman.y - ghost.pos.y; let preferredDirs: Direction[] = []; if (isPowered) { preferredDirs = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? ['left', 'up', 'down', 'right'] : ['right', 'down', 'up', 'left']) : (dy > 0 ? ['up', 'left', 'right', 'down'] : ['down', 'right', 'left', 'up']); } else { preferredDirs = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? ['right', 'down', 'up', 'left'] : ['left', 'up', 'down', 'right']) : (dy > 0 ? ['down', 'right', 'left', 'up'] : ['up', 'left', 'right', 'down']); } if (Math.random() < 0.6) { for (const dir of preferredDirs) { if (validDirs.includes(dir)) return { ...ghost, pos: moveEntity(ghost.pos, dir), dir }; } } const dir = validDirs[Math.floor(Math.random() * validDirs.length)]; return { ...ghost, pos: moveEntity(ghost.pos, dir), dir }; })); // Now check for collisions after all movement is complete // Check Player 1 collisions (from both player movement and ghost movement) if (!player1Eliminated) { setGhosts(currentGhosts => { for (let i = 0; i < currentGhosts.length; i++) { const ghost = currentGhosts[i]; if (ghost.pos.x === pacman.x && ghost.pos.y === pacman.y) { if (isPowered && !ghost.eaten) { // Player eats ghost setScore(prev => { const ns = Math.min(prev + 200, MAX_SCORE); if (ns >= MAX_SCORE) setShowGlitchCrash(true); if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); } return ns; }); playSound('success'); return currentGhosts.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g); } else if (!ghost.eaten) { // Ghost kills player const newLives = lives - 1; setLives(newLives); if (newLives > 0) { setPacman({ x: 10, y: 15 }); playSound('error'); } else { setPlayer1Eliminated(true); setPacman({ x: -1, y: -1 }); playSound('error'); if (gameMode === '2p' && !player2Eliminated) { setShowEliminationPrompt(true); setIsPaused(true); } else { setGameOver(true); } } } } } return currentGhosts; }); } // Check Player 2 collisions if (gameMode === '2p' && !player2Eliminated) { setGhosts(currentGhosts => { for (let i = 0; i < currentGhosts.length; i++) { const ghost = currentGhosts[i]; if (ghost.pos.x === pacman2.x && ghost.pos.y === pacman2.y) { if (isPowered && !ghost.eaten) { // Player eats ghost setScore2(prev => { const ns = Math.min(prev + 200, MAX_SCORE); if (ns >= MAX_SCORE) setShowGlitchCrash(true); if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); } return ns; }); playSound('success'); return currentGhosts.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g); } else if (!ghost.eaten) { // Ghost kills player const newLives = lives2 - 1; setLives2(newLives); if (newLives > 0) { setPacman2({ x: 10, y: 15 }); playSound('error'); } else { setPlayer2Eliminated(true); setPacman2({ x: -1, y: -1 }); playSound('error'); if (!player1Eliminated) { setShowEliminationPrompt(true); setIsPaused(true); } else { setGameOver(true); } } } } } return currentGhosts; }); } }, TICK_SPEED); return () => clearInterval(interval); }, [gameStarted, gameOver, gameComplete, isPaused, pacman, direction, nextDirection, pacman2, direction2, nextDirection2, dots, powerPellets, highScore, isPowered, playSound, gameMode, player1Eliminated, player2Eliminated]); // More responsive input - update direction immediately when pressed useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!gameStarted || gameOver || gameComplete || isPaused) return; // Player 1 controls (WASD and Arrow keys) - immediate response switch (e.key) { // In 2P mode, P1 uses WASD only; in 1P mode, use both WASD and Arrows case 'ArrowUp': e.preventDefault(); if (gameMode === '1p' && !player1Eliminated) setNextDirection('up'); break; case 'ArrowDown': e.preventDefault(); if (gameMode === '1p' && !player1Eliminated) setNextDirection('down'); break; case 'ArrowLeft': e.preventDefault(); if (gameMode === '1p' && !player1Eliminated) setNextDirection('left'); break; case 'ArrowRight': e.preventDefault(); if (gameMode === '1p' && !player1Eliminated) setNextDirection('right'); break; case 'w': case 'W': e.preventDefault(); if (!player1Eliminated) setNextDirection('up'); break; case 's': case 'S': e.preventDefault(); if (!player1Eliminated) setNextDirection('down'); break; case 'a': case 'A': e.preventDefault(); if (!player1Eliminated) setNextDirection('left'); break; case 'd': case 'D': e.preventDefault(); if (!player1Eliminated) setNextDirection('right'); break; } // Player 2 controls (Arrow keys) - only in 2P mode // P1 uses WASD, P2 uses Arrow keys if (gameMode === '2p') { switch (e.key) { case 'ArrowUp': e.preventDefault(); if (!player2Eliminated) setNextDirection2('up'); break; case 'ArrowDown': e.preventDefault(); if (!player2Eliminated) setNextDirection2('down'); break; case 'ArrowLeft': e.preventDefault(); if (!player2Eliminated) setNextDirection2('left'); break; case 'ArrowRight': e.preventDefault(); if (!player2Eliminated) setNextDirection2('right'); break; } } }; const handleKeyUp = (e: KeyboardEvent) => { if (!gameStarted || gameOver || gameComplete || isPaused) return; // Pause (works for both modes) if (e.key === 'p' || e.key === 'P') { e.preventDefault(); if (!gameOver && !gameComplete) setIsPaused(prev => !prev); } }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); }; }, [gameStarted, gameOver, gameComplete, isPaused, gameMode, player1Eliminated, player2Eliminated]); const getPacmanRotation = () => { switch (direction) { case 'right': return 0; case 'down': return 90; case 'left': return 180; case 'up': return 270; } }; const getPacmanRotation2 = () => { switch (direction2) { case 'right': return 0; case 'down': return 90; case 'left': return 180; case 'up': return 270; } }; const renderPacman = () => ( {mouthOpen && } ); const renderPacman2 = () => ( {mouthOpen2 && } ); const renderGhost = (index: number, eaten: boolean) => { const colors = ['hsl(0 70% 50%)', 'hsl(300 70% 50%)', 'hsl(180 70% 50%)']; const scaredColor = 'hsl(220 70% 50%)'; if (eaten) return null; return ( ); }; const renderGrid = () => { const cells = []; const spriteSize = Math.max(cellSize * 0.7, 12); for (let y = 0; y < GRID_HEIGHT; y++) { for (let x = 0; x < GRID_WIDTH; x++) { const isWall = MAZE_TEMPLATE[y][x] === 1; const isTunnel = MAZE_TEMPLATE[y][x] === 2; const isPacmanHere = pacman.x === x && pacman.y === y; const isPacman2Here = gameMode === '2p' && pacman2.x === x && pacman2.y === y; const ghostIndex = ghosts.findIndex(g => g.pos.x === x && g.pos.y === y && !g.eaten); const isDot = dots.has(`${x},${y}`); const isPowerPellet = powerPellets.has(`${x},${y}`); cells.push(
{isPacmanHere &&
{renderPacman()}
} {isPacman2Here &&
{renderPacman2()}
} {ghostIndex !== -1 && !isPacmanHere && !isPacman2Here &&
{renderGhost(ghostIndex, ghosts[ghostIndex].eaten)}
} {isDot && !isPacmanHere && !isPacman2Here && ghostIndex === -1 &&
} {isPowerPellet && !isPacmanHere && !isPacman2Here && ghostIndex === -1 &&
}
); } } return cells; }; if (showGlitchCrash) return window.location.reload()} />; return (
{'<'} Back

Pac-Man

{renderGrid()}
{!isMobile && (
{/* Score Panel */} {gameMode === '2p' ? (

Scores

P1

{score.toLocaleString()}

P2

{score2.toLocaleString()}

) : (

Score

{score.toLocaleString()}

)} {/* High Score */}

High Score

{highScore.toLocaleString()}

max: 4,294,967,296

{/* Level & Lives */}

Level

{level}

{gameMode === '2p' ? (

Lives

{lives} / {lives2}
) : (

Lives

{'♥'.repeat(lives)}

)}
{/* Controls */}

Controls

{gameMode === '2p' ? (

P1

WASD

P2

IJKL

) : (

WASD / ←→↑↓

P: Pause

)} {gameMode === '2p' &&

P: Pause

}
{/* Action Buttons */} {showModeSelection ? (

Select Mode

) : !gameStarted || gameOver || gameComplete ? ( ) : ( )}
)} {isMobile && ( <>
{gameMode === '2p' ? ( <>

P1

{score.toLocaleString()}

P2

{score2.toLocaleString()}

LVL

{level}

) : ( <>

SCORE

{score.toLocaleString()}

HIGH

{highScore.toLocaleString()}

LVL

{level}

)} {gameMode === '2p' && ( <>

P1

{lives}

P2

{lives2}

)} {gameMode === '1p' && (

LIVES

{lives}

)}
{showModeSelection ? (

MOBILE: 1 PLAYER ONLY

) : gameStarted && !gameOver && !gameComplete ? ( useButtonControls ? (
) : (

SWIPE TO MOVE
TAP TO PAUSE

) ) : ( )}
)}
{!isMobile && showEliminationPrompt && gameStarted && (

{player1Eliminated ? 'PLAYER 2 WON!' : 'PLAYER 1 WON!'}

{player1Eliminated ? 'Player 1' : 'Player 2'} has run out of lives.
Continue with the remaining player?

)} {!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && !showEliminationPrompt && (

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

{(gameOver || gameComplete) && ( <> {gameMode === '2p' ? ( <>

Final Scores

P1

{score.toLocaleString()}

P2

{score2.toLocaleString()}

) : (

Final Score: {score.toLocaleString()}

)}

Level: {level}

)}
)}
); }; export default Pacman;