Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Modern Pac-Man</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --pacman-yellow: #FFCC00; | |
| --ghost-blinky: #FF0000; | |
| --ghost-pinky: #FFB8FF; | |
| --ghost-inky: #00FFFF; | |
| --ghost-clyde: #FFB852; | |
| --wall-blue: #2121DE; | |
| --bg-dark: #0a0a0a; | |
| --neon-glow: 0 0 20px currentColor, 0 0 40px currentColor, 0 0 60px currentColor; | |
| } | |
| body { | |
| min-height: 100vh; | |
| background: var(--bg-dark); | |
| font-family: 'Rajdhani', sans-serif; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* Animated background */ | |
| .bg-grid { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: | |
| linear-gradient(rgba(33, 33, 222, 0.1) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(33, 33, 222, 0.1) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| animation: gridMove 20s linear infinite; | |
| pointer-events: none; | |
| } | |
| @keyframes gridMove { | |
| 0% { transform: perspective(500px) rotateX(60deg) translateY(0); } | |
| 100% { transform: perspective(500px) rotateX(60deg) translateY(50px); } | |
| } | |
| /* Header */ | |
| .header { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| padding: 15px 30px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: linear-gradient(180deg, rgba(0,0,0,0.9) 0%, transparent 100%); | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(1.2rem, 3vw, 2rem); | |
| font-weight: 900; | |
| color: var(--pacman-yellow); | |
| text-shadow: var(--neon-glow); | |
| letter-spacing: 3px; | |
| } | |
| .built-with { | |
| font-size: 0.85rem; | |
| color: #888; | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| } | |
| .built-with:hover { | |
| color: var(--pacman-yellow); | |
| } | |
| /* Game Container */ | |
| .game-wrapper { | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 20px; | |
| padding: 20px; | |
| z-index: 10; | |
| } | |
| .score-board { | |
| display: flex; | |
| gap: 40px; | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(1rem, 2.5vw, 1.5rem); | |
| color: #fff; | |
| text-shadow: 0 0 10px currentColor; | |
| } | |
| .score-item { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .score-label { | |
| font-size: 0.7rem; | |
| color: #888; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .score-value { | |
| color: var(--pacman-yellow); | |
| font-weight: 700; | |
| min-width: 80px; | |
| text-align: center; | |
| } | |
| .high-score { | |
| color: #00FFFF; | |
| } | |
| /* Canvas Container */ | |
| .canvas-container { | |
| position: relative; | |
| border-radius: 20px; | |
| padding: 4px; | |
| background: linear-gradient(135deg, var(--wall-blue), #0000FF); | |
| box-shadow: | |
| 0 0 60px rgba(33, 33, 222, 0.5), | |
| inset 0 0 60px rgba(0,0,0,0.5); | |
| } | |
| canvas { | |
| display: block; | |
| border-radius: 16px; | |
| background: #000; | |
| max-width: 100%; | |
| height: auto; | |
| } | |
| /* Controls */ | |
| .controls { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .btn { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1rem; | |
| font-weight: 700; | |
| padding: 15px 40px; | |
| border: none; | |
| border-radius: 50px; | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--pacman-yellow), #FF9500); | |
| color: #000; | |
| box-shadow: 0 0 30px rgba(255, 204, 0, 0.4); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 10px 40px rgba(255, 204, 0, 0.6); | |
| } | |
| .btn-primary::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); | |
| transition: left 0.5s; | |
| } | |
| .btn-primary:hover::before { | |
| left: 100%; | |
| } | |
| .instructions { | |
| display: flex; | |
| gap: 30px; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .key { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| color: #888; | |
| font-size: 0.9rem; | |
| } | |
| .key span { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 35px; | |
| height: 35px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 8px; | |
| border: 1px solid rgba(255,255,255,0.2); | |
| color: #fff; | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 0.8rem; | |
| } | |
| /* Mobile Controls */ | |
| .mobile-controls { | |
| display: none; | |
| grid-template-columns: repeat(3, 70px); | |
| gap: 10px; | |
| margin-top: 10px; | |
| } | |
| .mobile-btn { | |
| width: 70px; | |
| height: 70px; | |
| border-radius: 50%; | |
| border: 2px solid var(--wall-blue); | |
| background: rgba(33, 33, 222, 0.2); | |
| color: #fff; | |
| font-size: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| user-select: none; | |
| } | |
| .mobile-btn:active { | |
| background: var(--wall-blue); | |
| transform: scale(0.95); | |
| } | |
| .mobile-btn:nth-child(2) { grid-column: 2; } | |
| .mobile-btn:nth-child(3) { grid-column: 1; grid-row: 2; } | |
| .mobile-btn:nth-child(4) { grid-column: 2; grid-row: 2; } | |
| .mobile-btn:nth-child(5) { grid-column: 3; grid-row: 2; } | |
| /* Overlay Screens */ | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| background: rgba(0,0,0,0.9); | |
| border-radius: 16px; | |
| z-index: 50; | |
| transition: opacity 0.3s; | |
| } | |
| .overlay.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .overlay h2 { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(2rem, 5vw, 3.5rem); | |
| color: var(--pacman-yellow); | |
| text-shadow: var(--neon-glow); | |
| margin-bottom: 20px; | |
| text-align: center; | |
| } | |
| .overlay p { | |
| color: #888; | |
| margin-bottom: 30px; | |
| text-align: center; | |
| max-width: 400px; | |
| line-height: 1.6; | |
| } | |
| .game-over h2 { | |
| color: #FF0000; | |
| text-shadow: 0 0 20px #FF0000, 0 0 40px #FF0000; | |
| } | |
| .level-complete h2 { | |
| color: #00FF00; | |
| text-shadow: 0 0 20px #00FF00, 0 0 40px #00FF00; | |
| } | |
| /* Lives display */ | |
| .lives { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .life { | |
| width: 25px; | |
| height: 25px; | |
| background: var(--pacman-yellow); | |
| border-radius: 50%; | |
| clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 85%, 0 100%); | |
| animation: pulse 1s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.1); } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 600px) { | |
| .mobile-controls { | |
| display: grid; | |
| } | |
| .instructions { | |
| display: none; | |
| } | |
| .score-board { | |
| gap: 20px; | |
| } | |
| } | |
| /* Sound toggle */ | |
| .sound-toggle { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| border: 2px solid var(--wall-blue); | |
| background: rgba(0,0,0,0.8); | |
| color: var(--wall-blue); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s; | |
| z-index: 100; | |
| } | |
| .sound-toggle:hover, .sound-toggle.active { | |
| background: var(--wall-blue); | |
| color: #fff; | |
| box-shadow: 0 0 20px var(--wall-blue); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-grid"></div> | |
| <header class="header"> | |
| <div class="logo">◉ PAC-MAN</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with">Built with anycoder</a> | |
| </header> | |
| <div class="game-wrapper"> | |
| <div class="score-board"> | |
| <div class="score-item"> | |
| <span class="score-label">Score</span> | |
| <span class="score-value" id="score">0</span> | |
| </div> | |
| <div class="score-item"> | |
| <span class="score-label">High Score</span> | |
| <span class="score-value high-score" id="highScore">0</span> | |
| </div> | |
| <div class="score-item"> | |
| <span class="score-label">Level</span> | |
| <span class="score-value" id="level">1</span> | |
| </div> | |
| <div class="score-item"> | |
| <span class="score-label">Lives</span> | |
| <div class="lives" id="lives"></div> | |
| </div> | |
| </div> | |
| <div class="canvas-container"> | |
| <canvas id="gameCanvas" width="570" height="630"></canvas> | |
| <div class="overlay" id="startScreen"> | |
| <h2>READY?</h2> | |
| <p>Use arrow keys or WASD to move. Eat all dots and avoid ghosts! Power pellets let you eat ghosts.</p> | |
| <button class="btn btn-primary" onclick="startGame()">Start Game</button> | |
| </div> | |
| <div class="overlay game-over hidden" id="gameOverScreen"> | |
| <h2>GAME OVER</h2> | |
| <p id="finalScore">Final Score: 0</p> | |
| <button class="btn btn-primary" onclick="restartGame()">Try Again</button> | |
| </div> | |
| <div class="overlay level-complete hidden" id="levelCompleteScreen"> | |
| <h2>LEVEL COMPLETE!</h2> | |
| <p>Get ready for the next level...</p> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <div class="instructions"> | |
| <div class="key"><span>↑</span> Up</div> | |
| <div class="key"><span>↓</span> Down</div> | |
| <div class="key"><span>←</span> Left</div> | |
| <div class="key"><span>→</span> Right</div> | |
| <div class="key"><span>P</span> Pause</div> | |
| </div> | |
| <div class="mobile-controls"> | |
| <div class="mobile-btn" data-dir="up">▲</div> | |
| <div class="mobile-btn" data-dir="left">◀</div> | |
| <div class="mobile-btn" data-dir="down">▼</div> | |
| <div class="mobile-btn" data-dir="right">▶</div> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="sound-toggle" id="soundToggle" onclick="toggleSound()"> | |
| <span id="soundIcon">🔇</span> | |
| </button> | |
| <script> | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Game constants | |
| const CELL_SIZE = 30; | |
| const COLS = 19; | |
| const ROWS = 21; | |
| const PACMAN_SPEED = 2; | |
| const GHOST_SPEED = 1.8; | |
| // Game state | |
| let gameState = 'start'; // start, playing, paused, gameover, levelcomplete | |
| let score = 0; | |
| let highScore = localStorage.getItem('pacmanHighScore') || 0; | |
| let level = 1; | |
| let lives = 3; | |
| let dots = []; | |
| let powerPellets = []; | |
| let particles = []; | |
| // Map layout (0=empty, 1=wall, 2=dot, 3=power, 4=ghost house) | |
| const mapLayout = [ | |
| [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], | |
| [1,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,1], | |
| [1,3,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,3,1], | |
| [1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1], | |
| [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], | |
| [1,2,1,1,2,1,2,1,1,1,1,1,2,1,2,1,1,2,1], | |
| [1,2,2,2,2,1,2,2,2,1,2,2,2,1,2,2,2,2,1], | |
| [1,1,1,1,2,1,1,1,0,1,0,1,1,1,2,1,1,1,1], | |
| [0,0,0,1,2,1,0,0,0,0,0,0,0,1,2,1,0,0,0], | |
| [1,1,1,1,2,1,0,1,1,4,1,1,0,1,2,1,1,1,1], | |
| [0,0,0,0,2,0,0,1,4,4,4,1,0,0,2,0,0,0,0], | |
| [1,1,1,1,2,1,0,1,1,1,1,1,0,1,2,1,1,1,1], | |
| [0,0,0,1,2,1,0,0,0,0,0,0,0,1,2,1,0,0,0], | |
| [1,1,1,1,2,1,0,1,1,1,1,1,0,1,2,1,1,1,1], | |
| [1,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,1], | |
| [1,2,1,1,2,1,1,1,2,1,2,1,1,1,2,1,1,2,1], | |
| [1,3,2,1,2,2,2,2,2,0,2,2,2,2,2,1,2,3,1], | |
| [1,1,2,1,2,1,2,1,1,1,1,1,2,1,2,1,2,1,1], | |
| [1,2,2,2,2,1,2,2,2,1,2,2,2,1,2,2,2,2,1], | |
| [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] | |
| ]; | |
| // Pac-Man | |
| const pacman = { | |
| x: 9 * CELL_SIZE + CELL_SIZE / 2, | |
| y: 15 * CELL_SIZE + CELL_SIZE / 2, | |
| dir: { x: 0, y: 0 }, | |
| nextDir: { x: 0, y: 0 }, | |
| mouthOpen: 0, | |
| mouthSpeed: 0.15, | |
| angle: 0, | |
| powered: false, | |
| poweredTime: 0 | |
| }; | |
| // Ghosts | |
| const ghosts = [ | |
| { name: 'blinky', color: '#FF0000', x: 9 * CELL_SIZE + CELL_SIZE / 2, y: 9 * CELL_SIZE + CELL_SIZE / 2, dir: { x: 0, y: -1 }, mode: 'chase', scared: false, eyes: false }, | |
| { name: 'pinky', color: '#FFB8FF', x: 9 * CELL_SIZE + CELL_SIZE / 2, y: 10 * CELL_SIZE + CELL_SIZE / 2, dir: { x: 0, y: -1 }, mode: 'chase', scared: false, eyes: false }, | |
| { name: 'inky', color: '#00FFFF', x: 8 * CELL_SIZE + CELL_SIZE / 2, y: 10 * CELL_SIZE + CELL_SIZE / 2, dir: { x: 0, y: -1 }, mode: 'chase', scared: false, eyes: false }, | |
| { name: 'clyde', color: '#FFB852', x: 10 * CELL_SIZE + CELL_SIZE / 2, y: 10 * CELL_SIZE + CELL_SIZE / 2, dir: { x: 0, y: -1 }, mode: 'chase', scared: false, eyes: false } | |
| ]; | |
| // Sound synthesis | |
| const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| let soundEnabled = false; | |
| function playSound(type) { | |
| if (!soundEnabled) return; | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); | |
| gain.connect(audioCtx.destination); | |
| const now = audioCtx.currentTime; | |
| switch(type) { | |
| case 'waka': | |
| osc.frequency.setValueAtTime(800, now); | |
| osc.frequency.exponentialRampToValueAtTime(400, now + 0.1); | |
| gain.gain.setValueAtTime(0.1, now); | |
| gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); | |
| osc.start(now); | |
| osc.stop(now + 0.1); | |
| break; | |
| case 'power': | |
| osc.frequency.setValueAtTime(200, now); | |
| osc.frequency.linearRampToValueAtTime(800, now + 0.3); | |
| gain.gain.setValueAtTime(0.2, now); | |
| gain.gain.linearRampToValueAtTime(0, now + 0.3); | |
| osc.start(now); | |
| osc.stop(now + 0.3); | |
| break; | |
| case 'eatGhost': | |
| osc.frequency.setValueAtTime(400, now); | |
| osc.frequency.exponentialRampToValueAtTime(800, now + 0.2); | |
| gain.gain.setValueAtTime(0.3, now); | |
| gain.gain.exponentialRampToValueAtTime(0, now + 0.2); | |
| osc.start(now); | |
| osc.stop(now + 0.2); | |
| break; | |
| case 'die': | |
| osc.frequency.setValueAtTime(400, now); | |
| osc.frequency.exponentialRampToValueAtTime(50, now + 0.5); | |
| gain.gain.setValueAtTime(0.3, now); | |
| gain.gain.exponentialRampToValueAtTime(0, now + 0.5); | |
| osc.start(now); | |
| osc.stop(now + 0.5); | |
| break; | |
| } | |
| } | |
| function toggleSound() { | |
| soundEnabled = !soundEnabled; | |
| document.getElementById('soundToggle').classList.toggle('active', soundEnabled); | |
| document.getElementById('soundIcon').textContent = soundEnabled ? '🔊' : '🔇'; | |
| } | |
| // Initialize game | |
| function init() { | |
| document.getElementById('highScore').textContent = highScore; | |
| updateLives(); | |
| createMap(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function createMap() { | |
| dots = []; | |
| powerPellets = []; | |
| for (let row = 0; row < ROWS; row++) { | |
| for (let col = 0; col < COLS; col++) { | |
| if (mapLayout[row] && mapLayout[row][col] === 2) { | |
| dots.push({ x: col * CELL_SIZE + CELL_SIZE / 2, y: row * CELL_SIZE + CELL_SIZE / 2, eaten: false }); | |
| } else if (mapLayout[row] && mapLayout[row][col] === 3) { | |
| powerPellets.push({ x: col * CELL_SIZE + CELL_SIZE / 2, y: row * CELL_SIZE + CELL_SIZE / 2, eaten: false, pulse: 0 }); | |
| } | |
| } | |
| } | |
| } | |
| function updateLives() { | |
| const livesContainer = document.getElementById('lives'); | |
| livesContainer.innerHTML = ''; | |
| for (let i = 0; i < lives; i++) { | |
| livesContainer.innerHTML += '<div class="life"></div>'; | |
| } | |
| } | |
| function startGame() { | |
| document.getElementById('startScreen').classList.add('hidden'); | |
| gameState = 'playing'; | |
| resetPositions(); | |
| } | |
| function restartGame() { | |
| score = 0; | |
| level = 1; | |
| lives = 3; | |
| document.getElementById('score').textContent = '0'; | |
| document.getElementById('level').textContent = '1'; | |
| updateLives(); | |
| createMap(); | |
| document.getElementById('gameOverScreen').classList.add('hidden'); | |
| gameState = 'playing'; | |
| resetPositions(); | |
| } | |
| function resetPositions() { | |
| pacman.x = 9 * CELL_SIZE + CELL_SIZE / 2; | |
| pacman.y = 15 * CELL_SIZE + CELL_SIZE / 2; | |
| pacman.dir = { x: 0, y: 0 }; | |
| pacman.nextDir = { x: 0, y: 0 }; | |
| pacman.powered = false; | |
| ghosts[0].x = 9 * CELL_SIZE + CELL_SIZE / 2; | |
| ghosts[0].y = 9 * CELL_SIZE + CELL_SIZE / 2; | |
| ghosts[1].x = 9 * CELL_SIZE + CELL_SIZE / 2; | |
| ghosts[1].y = 10 * CELL_SIZE + CELL_SIZE / 2; | |
| ghosts[2].x = 8 * CELL_SIZE + CELL_SIZE / 2; | |
| ghosts[2].y = 10 * CELL_SIZE + CELL_SIZE / 2; | |
| ghosts[3].x = 10 * CELL_SIZE + CELL_SIZE / 2; | |
| ghosts[3].y = 10 * CELL_SIZE + CELL_SIZE / 2; | |
| ghosts.forEach(g => { | |
| g.dir = { x: 0, y: -1 }; | |
| g.scared = false; | |
| g.eyes = false; | |
| }); | |
| } | |
| // Input handling | |
| const keys = {}; | |
| document.addEventListener('keydown', (e) => { | |
| keys[e.key] = true; | |
| if (gameState !== 'playing') return; | |
| switch(e.key) { | |
| case 'ArrowUp': | |
| case 'w': | |
| case 'W': | |
| pacman.nextDir = { x: 0, y: -1 }; | |
| break; | |
| case 'ArrowDown': | |
| case 's': | |
| case 'S': | |
| pacman.nextDir = { x: 0, y: 1 }; | |
| break; | |
| case 'ArrowLeft': | |
| case 'a': | |
| case 'A': | |
| pacman.nextDir = { x: -1, y: 0 }; | |
| break; | |
| case 'ArrowRight': | |
| case 'd': | |
| case 'D': | |
| pacman.nextDir = { x: 1, y: 0 }; | |
| break; | |
| case 'p': | |
| case 'P': | |
| gameState = gameState === 'paused' ? 'playing' : 'paused'; | |
| break; | |
| } | |
| }); | |
| // Mobile controls | |
| document.querySelectorAll('.mobile-btn').forEach(btn => { | |
| btn.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| const dir = btn.dataset.dir; | |
| switch(dir) { | |
| case 'up': pacman.nextDir = { x: 0, y: -1 }; break; | |
| case 'down': pacman.nextDir = { x: 0, y: 1 }; break; | |
| case 'left': pacman.nextDir = { x: -1, y: 0 }; break; | |
| case 'right': pacman.nextDir = { x: 1, y: 0 }; break; | |
| } | |
| }); | |
| }); | |
| function canMove(x, y, dir) { | |
| const nextX = x + dir.x * PACMAN_SPEED; | |
| const nextY = y + dir.y * PACMAN_SPEED; | |
| const col = Math.floor(nextX / CELL_SIZE); | |
| const row = Math.floor(nextY / CELL_SIZE); | |
| if (row < 0 || row >= ROWS || col < 0 || col >= COLS) { | |
| // Tunnel wrap | |
| if (col < 0) return { can: true, wrap: 'left' }; | |
| if (col >= COLS) return { can: true, wrap: 'right' }; | |
| return { can: false }; | |
| } | |
| if (!mapLayout[row] || mapLayout[row][col] === 1) { | |
| return { can: false }; | |
| } | |
| // Check if in center of cell for turning | |
| const centerX = col * CELL_SIZE + CELL_SIZE / 2; | |
| const centerY = row * CELL_SIZE + CELL_SIZE / 2; | |
| const distToCenter = Math.sqrt((nextX - centerX) ** 2 + (nextY - centerY) ** 2); | |
| return { can: true, inCenter: distToCenter < 5 }; | |
| } | |
| function update() { | |
| if (gameState !== 'playing') return; | |
| // Update power pellet pulse | |
| powerPellets.forEach(p => p.pulse += 0.1); | |
| // Update Pac-Man | |
| const moveCheck = canMove(pacman.x, pacman.y, pacman.nextDir); | |
| if (moveCheck.can && (pacman.dir.x === 0 && pacman.dir.y === 0 || moveCheck.inCenter)) { | |
| pacman.dir = { ...pacman.nextDir }; | |
| } | |
| const currentMove = canMove(pacman.x, pacman.y, pacman.dir); | |
| if (currentMove.can) { | |
| if (currentMove.wrap === 'left') { | |
| pacman.x = COLS * CELL_SIZE - 1; | |
| } else if (currentMove.wrap === 'right') { | |
| pacman.x = 0; | |
| } else { | |
| pacman.x += pacman.dir.x * PACMAN_SPEED; | |
| pacman.y += pacman.dir.y * PACMAN_SPEED; | |
| } | |
| // Animate mouth | |
| pacman.mouthOpen += pacman.mouthSpeed; | |
| if (pacman.mouthOpen > 1 || pacman.mouthOpen < 0) { | |
| pacman.mouthSpeed = -pacman.mouthSpeed; | |
| } | |
| } | |
| // Update angle based on direction | |
| if (pacman.dir.x === 1) pacman.angle = 0; | |
| else if (pacman.dir.x === -1) pacman.angle = Math.PI; | |
| else if (pacman.dir.y === -1) pacman.angle = -Math.PI / 2; | |
| else if (pacman.dir.y === 1) pacman.angle = Math.PI / 2; | |
| // Eat dots | |
| const pacCol = Math.floor(pacman.x / CELL_SIZE); | |
| const pacRow = Math.floor(pacman.y / CELL_SIZE); | |
| dots.forEach(dot => { | |
| if (!dot.eaten) { | |
| const dist = Math.sqrt((pacman.x - dot.x) ** 2 + (pacman.y - dot.y) ** 2); | |
| if (dist < 15) { | |
| dot.eaten = true; | |
| score += 10; | |
| document.getElementById('score').textContent = score; | |
| playSound('waka'); | |
| createParticles(dot.x, dot.y, '#FFB897'); | |
| } | |
| } | |
| }); | |
| // Eat power pellets | |
| powerPellets.forEach(pellet => { | |
| if (!pellet.eaten) { | |
| const dist = Math.sqrt((pacman.x - pellet.x) ** 2 + (pacman.y - pellet.y) ** 2); | |
| if (dist < 15) { | |
| pellet.eaten = true; | |
| score += 50; | |
| document.getElementById('score').textContent = score; | |
| pacman.powered = true; | |
| pacman.poweredTime = 600; // 10 seconds at 60fps | |
| ghosts.forEach(g => { if (!g.eyes) g.scared = true; }); | |
| playSound('power'); | |
| createParticles(pellet.x, pellet.y, '#FFCC00', 10); | |
| } | |
| } | |
| }); | |
| // Update power mode | |
| if (pacman.powered) { | |
| pacman.poweredTime--; | |
| if (pacman.poweredTime <= 0) { | |
| pacman.powered = false; | |
| ghosts.forEach(g => g.scared = false); | |
| } else if (pacman.poweredTime < 120) { | |
| // Flash when about to end | |
| ghosts.forEach(g => g.scared = Math.floor(pacman.poweredTime / 10) % 2 === 0); | |
| } | |
| } | |
| // Update ghosts | |
| ghosts.forEach((ghost, i) => { | |
| if (ghost.eyes) { | |
| // Return to ghost house | |
| const targetX = 9 * CELL_SIZE + CELL_SIZE / 2; | |
| const targetY = 9 * CELL_SIZE + CELL_SIZE / 2; | |
| moveGhost(ghost, targetX, targetY, GHOST_SPEED * 2); | |
| const distToHouse = Math.sqrt((ghost.x - targetX) ** 2 + (ghost.y - targetY) ** 2); | |
| if (distToHouse < 5) { | |
| ghost.eyes = false; | |
| ghost.scared = false; | |
| } | |
| } else { | |
| // AI targeting | |
| let targetX, targetY; | |
| if (ghost.scared) { | |
| // Run away randomly | |
| targetX = Math.random() * canvas.width; | |
| targetY = Math.random() * canvas.height; | |
| } else { | |
| // Chase Pac-Man with different personalities | |
| switch(ghost.name) { | |
| case 'blinky': | |
| targetX = pacman.x; | |
| targetY = pacman.y; | |
| break; | |
| case 'pinky': | |
| targetX = pacman.x + pacman.dir.x * 4 * CELL_SIZE; | |
| targetY = pacman.y + pacman.dir.y * 4 * CELL_SIZE; | |
| break; | |
| case 'inky': | |
| targetX = pacman.x + pacman.dir.x * 2 * CELL_SIZE; | |
| targetY = pacman.y + pacman.dir.y * 2 * CELL_SIZE; | |
| // Add some randomness | |
| targetX += (Math.random() - 0.5) * 100; | |
| break; | |
| case 'clyde': | |
| const dist = Math.sqrt((ghost.x - pacman.x) ** 2 + (ghost.y - pacman.y) ** 2); | |
| if (dist > 8 * CELL_SIZE) { | |
| targetX = pacman.x; | |
| targetY = pacman.y; | |
| } else { | |
| targetX = 0; | |
| targetY = canvas.height; | |
| } | |
| break; | |
| } | |
| } | |
| moveGhost(ghost, targetX, targetY, ghost.scared ? GHOST_SPEED * 0.6 : GHOST_SPEED + level * 0.1); | |
| } | |
| // Collision with Pac-Man | |
| const dist = Math.sqrt((ghost.x - pacman.x) ** 2 + (ghost.y - pacman.y) ** 2); | |
| if (dist < 20) { | |
| if (ghost.scared && !ghost.eyes) { | |
| ghost.eyes = true; | |
| ghost.scared = false; | |
| score += 200 * (Math.pow(2, ghosts.filter(g => g.eyes).length - 1)); | |
| document.getElementById('score').textContent = score; | |
| playSound('eatGhost'); | |
| createParticles(ghost.x, ghost.y, '#00FFFF', 15); | |
| } else if (!ghost.eyes) { | |
| // Pac-Man dies | |
| lives--; | |
| updateLives(); | |
| playSound('die'); | |
| createParticles(pacman.x, pacman.y, '#FFCC00', 20); | |
| if (lives <= 0) { | |
| gameOver(); | |
| } else { | |
| resetPositions(); | |
| } | |
| } | |
| } | |
| }); | |
| // Update particles | |
| particles = particles.filter(p => { | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.vy += 0.1; | |
| p.life--; | |
| return p.life > 0; | |
| }); | |
| // Check level complete | |
| if (dots.every(d => d.eaten) && powerPellets.every(p => p.eaten)) { | |
| levelComplete(); | |
| } | |
| // Update high score | |
| if (score > highScore) { | |
| highScore = score; | |
| localStorage.setItem('pacmanHighScore', highScore); | |
| document.getElementById('highScore').textContent = highScore; | |
| } | |
| } | |
| function moveGhost(ghost, targetX, targetY, speed) { | |
| const dx = targetX - ghost.x; | |
| const dy = targetY - ghost.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist > 0) { | |
| const vx = (dx / dist) * speed; | |
| const vy = (dy / dist) * speed; | |
| // Try to move, with simple collision | |
| const nextX = ghost.x + vx; | |
| const nextY = ghost.y + vy; | |
| const col = Math.floor(nextX / CELL_SIZE); | |
| const row = Math.floor(nextY / CELL_SIZE); | |
| if (row >= 0 && row < ROWS && col >= 0 && col < COLS && | |
| mapLayout[row] && mapLayout[row][col] !== 1) { | |
| ghost.x = nextX; | |
| ghost.y = nextY; | |
| ghost.dir = { x: Math.sign(vx) || ghost.dir.x, y: Math.sign(vy) || ghost.dir.y }; | |
| } else { | |
| // Try to slide along walls | |
| const altMoves = [ | |
| { x: speed, y: 0 }, | |
| { x: -speed, y: 0 }, | |
| { x: 0, y: speed }, | |
| { x: 0, y: -speed } | |
| ]; | |
| for (let move of altMoves) { | |
| const testCol = Math.floor((ghost.x + move.x) / CELL_SIZE); | |
| const testRow = Math.floor((ghost.y + move.y) / CELL_SIZE); | |
| if (testRow >= 0 && testRow < ROWS && testCol >= 0 && testCol < COLS && | |
| mapLayout[testRow] && mapLayout[testRow][testCol] !== 1) { | |
| ghost.x += move.x; | |
| ghost.y += move.y; | |
| ghost.dir = { x: Math.sign(move.x), y: Math.sign(move.y) }; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function createParticles(x, y, color, count = 5) { | |
| for (let i = 0; i < count; i++) { | |
| particles.push({ | |
| x, y, | |
| vx: (Math.random() - 0.5) * 8, | |
| vy: (Math.random() - 0.5) * 8, | |
| life: 30 + Math.random() * 20, | |
| color, | |
| size: 2 + Math.random() * 4 | |
| }); | |
| } | |
| } | |
| function gameOver() { | |
| gameState = 'gameover'; | |
| document.getElementById('finalScore').textContent = `Final Score: ${score}`; | |
| document.getElementById('gameOverScreen').classList.remove('hidden'); | |
| } | |
| function levelComplete() { | |
| gameState = 'levelcomplete'; | |
| document.getElementById('levelCompleteScreen').classList.remove('hidden'); | |
| setTimeout(() => { | |
| level++; | |
| document.getElementById('level').textContent = level; | |
| createMap(); | |
| resetPositions(); | |
| document.getElementById('levelCompleteScreen').classList.add('hidden'); | |
| gameState = 'playing'; | |
| }, 2000); | |
| } | |
| function draw() { | |
| // Clear canvas | |
| ctx.fillStyle = '#000'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw maze walls with neon effect | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = '#2121DE'; | |
| ctx.fillStyle = '#2121DE'; | |
| for (let row = 0; row < ROWS; row++) { | |
| for (let col = 0; col < COLS; col++) { | |
| if (mapLayout[row] && mapLayout[row][col] === 1) { | |
| // Rounded walls | |
| const x = col * CELL_SIZE; | |
| const y = row * CELL_SIZE; | |
| const radius = 4; | |
| ctx.beginPath(); | |
| ctx.roundRect(x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2, radius); | |
| ctx.fill(); | |
| } | |
| } | |
| } | |
| ctx.shadowBlur = 0; | |
| // Draw dots | |
| ctx.fillStyle = '#FFB897'; | |
| dots.forEach(dot => { | |
| if (!dot.eaten) { | |
| ctx.beginPath(); | |
| ctx.arc(dot.x, dot.y, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| }); | |
| // Draw power pellets with pulse | |
| powerPellets.forEach(pellet => { | |
| if (!pellet.eaten) { | |
| const pulseSize = 6 + Math.sin(pellet.pulse) * 2; | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = '#FFCC00'; | |
| ctx.fillStyle = '#FFCC00'; | |
| ctx.beginPath(); | |
| ctx.arc(pellet.x, pellet.y, pulseSize, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| } | |
| }); | |
| // Draw Pac-Man | |
| ctx.save(); | |
| ctx.translate(pacman.x, pacman.y); | |
| ctx.rotate(pacman.angle); | |
| const mouthAngle = 0.2 + pacman.mouthOpen * 0.3; | |
| ctx.fillStyle = pacman.powered ? '#fff' : '#FFCC00'; | |
| ctx.shadowBlur = pacman.powered ? 30 : 20; | |
| ctx.shadowColor = pacman.powered ? '#fff' : '#FFCC00'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 12, mouthAngle, Math.PI * 2 - mouthAngle); | |
| ctx.lineTo(0, 0); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.restore(); | |
| ctx.shadowBlur = 0; | |
| // Draw ghosts | |
| ghosts.forEach(ghost => { | |
| ctx.save(); | |
| ctx.translate(ghost.x, ghost.y); | |
| if (ghost.eyes) { | |
| // Draw eyes only | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(-4, -2, 4, 0, Math.PI * 2); | |
| ctx.arc(4, -2, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = '#00f'; | |
| ctx.beginPath(); | |
| ctx.arc(-4, -2, 2, 0, Math.PI * 2); | |
| ctx.arc(4, -2, 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } else { | |
| // Ghost body with glow | |
| const color = ghost.scared ? '#2121DE' : ghost.color; | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = color; | |
| ctx.fillStyle = color; | |
| // Body shape | |
| ctx.beginPath(); | |
| ctx.arc(0, -2, 12, Math.PI, 0); | |
| ctx.lineTo(12, 10); | |
| // Wavy bottom | |
| for (let i = 0; i < 4; i++) { | |
| ctx.arc(12 - i * 6 - 3, 10 - (i % 2) * 4, 3, 0, Math.PI, i % 2 === 0); | |
| } | |
| ctx.lineTo(-12, 10); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Eyes | |
| ctx.fillStyle = '#fff'; | |
| ctx.shadowBlur = 0; | |
| ctx.beginPath(); | |
| ctx.arc(-4, -4, 4, 0, Math.PI * 2); | |
| ctx.arc(4, -4, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = ghost.scared ? '#fff' : '#00f'; | |
| const lookX = ghost.dir.x * 2; | |
| const lookY = ghost.dir.y * 2; | |
| ctx.beginPath(); | |
| ctx.arc(-4 + lookX, -4 + lookY, 2, 0, Math.PI * 2); | |
| ctx.arc(4 + lookX, -4 + lookY, 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Scared mouth | |
| if (ghost.scared |