diff --git a/assets/css/custom.css b/assets/css/custom.css index 50e0502..dc8bbf2 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,302 +1,316 @@ +:root { + --bg-dark: #0f172a; + --bg-mesh: #020617; + --card-bg: rgba(30, 41, 59, 0.7); + --accent: #38bdf8; + --accent-hover: #7dd3fc; + --text-main: #f8fafc; + --text-muted: #94a3b8; + --snake-head: #4ade80; + --snake-body: #166534; + --apple: #ef4444; + --wall: #475569; + --font-main: 'Inter', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --shadow-sm: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + font-family: var(--font-main); + background-color: var(--bg-dark); + color: var(--text-main); + line-height: 1.5; + min-height: 100vh; + display: flex; + flex-direction: column; + overflow-x: hidden; + position: relative; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +.background-mesh { + position: fixed; + inset: 0; + z-index: -1; + background-color: var(--bg-mesh); + background-image: + radial-gradient(at 0% 0%, rgba(56, 189, 248, 0.15) 0px, transparent 50%), + radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%), + radial-gradient(at 100% 100%, rgba(236, 72, 153, 0.1) 0px, transparent 50%), + radial-gradient(at 0% 100%, rgba(56, 189, 248, 0.1) 0px, transparent 50%); } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.container { + width: 100%; + max-width: 1100px; + margin: 0 auto; + padding: 0 1rem; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +.main-header { + padding: 1.5rem 0; + text-align: center; } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; +.main-header h1 { + font-size: 2.25rem; + font-weight: 800; + letter-spacing: -0.025em; + margin-bottom: 0.25rem; + background: linear-gradient(to right, var(--accent), #818cf8); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.badge { + font-size: 0.75rem; + padding: 0.2rem 0.6rem; + background: var(--accent); + border-radius: 9999px; + -webkit-text-fill-color: var(--bg-dark); + vertical-align: middle; + margin-left: 0.4rem; + text-transform: uppercase; + font-weight: 700; } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +.subtitle { + color: var(--text-muted); + font-size: 0.95rem; } -::-webkit-scrollbar-track { - background: transparent; +.main-layout { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding-bottom: 2rem; } -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; +@media (min-width: 968px) { + .main-layout { + display: grid; + grid-template-columns: 1fr 340px; + } } -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); +.card { + background: var(--card-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 1.25rem; + padding: 1.25rem; + box-shadow: var(--shadow-xl); + margin-bottom: 1rem; } -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +.card h3 { + font-size: 1.125rem; + margin-bottom: 1rem; + font-weight: 600; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding-bottom: 0.5rem; + color: var(--accent); } -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } +.canvas-wrapper { + position: relative; + background: #000; + border-radius: 1.25rem; + overflow: hidden; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1); + aspect-ratio: 1 / 1; + display: flex; + align-items: center; + justify-content: center; + max-width: 600px; + margin: 0 auto; } -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; +#gameCanvas { + width: 100%; + height: 100%; + touch-action: none; } -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; +.overlay { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + transition: opacity 0.3s ease; + backdrop-filter: blur(8px); } -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); +.overlay.hidden { + opacity: 0; + pointer-events: none; } -.chat-input-area form { - display: flex; - gap: 0.75rem; +.overlay-content h2 { + font-size: 2.5rem; + color: var(--snake-head); + margin-bottom: 0.5rem; + text-shadow: 0 0 15px rgba(74, 222, 128, 0.3); } -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; +.stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; } -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); +.stat-item { + background: rgba(15, 23, 42, 0.5); + padding: 0.75rem; + border-radius: 1rem; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.05); + transition: transform 0.2s; } -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; +.stat-item:hover { + transform: translateY(-2px); + border-color: rgba(56, 189, 248, 0.2); } -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); +.stat-item .label { + display: block; + font-size: 0.65rem; + text-transform: uppercase; + color: var(--text-muted); + font-weight: 700; + margin-bottom: 0.2rem; + letter-spacing: 0.05em; } -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; +.stat-item .value { + display: block; + font-size: 1.25rem; + font-family: var(--font-mono); + font-weight: 700; + color: var(--text-main); } -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); +.control-group { + margin-bottom: 1.25rem; } -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); +.control-group label { + display: block; + margin-bottom: 0.75rem; + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 600; } -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; +input[type="range"] { + width: 100%; + accent-color: var(--accent); + height: 6px; + border-radius: 3px; + cursor: pointer; } -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; +.range-labels { + display: flex; + justify-content: space-between; + margin-top: 0.5rem; + font-size: 0.7rem; + color: var(--text-muted); + font-weight: 600; } -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } +.button-group { + display: flex; + gap: 0.75rem; } -.admin-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; +.btn { + flex: 1; + padding: 0.75rem 1rem; + border-radius: 0.85rem; + font-weight: 700; + cursor: pointer; + transition: all 0.2s; + border: none; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.025em; } -.admin-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; +.btn-primary { + background: linear-gradient(135deg, var(--accent), #6366f1); + color: white; + box-shadow: 0 4px 15px rgba(56, 189, 248, 0.3); } -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(56, 189, 248, 0.4); } -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; +.btn-secondary { + background: rgba(255, 255, 255, 0.05); + color: var(--text-main); + border: 1px solid rgba(255, 255, 255, 0.1); } -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.1); } -.table th { - background: transparent; - border: none; +.info-card p { + font-size: 0.85rem; + color: var(--text-muted); + line-height: 1.6; +} + +.info-card strong { + color: var(--accent); +} + +.main-footer { + margin-top: auto; + padding: 2rem 0; + text-align: center; + color: var(--text-muted); + font-size: 0.8rem; +} + +@media (max-width: 640px) { + .main-header h1 { + font-size: 1.75rem; + } + + .canvas-wrapper { + border-radius: 1rem; + } + + .card { padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; + } + + .overlay-content h2 { + font-size: 2rem; + } } -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); +@media (max-height: 700px) and (orientation: landscape) { + .main-layout { + flex-direction: row; + } + .canvas-wrapper { + max-width: 400px; + } } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..a1fe791 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,599 @@ -document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); +/** + * Auto-Snake: AI-Powered Snake Game + * Implements Advanced Pathfinding (BFS + Longest Path Survival) + * to ensure the snake fills the grid and avoids infinite loops. + */ - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; +const CONFIG = { + GRID_SIZE: 20, + TILE_SIZE: 0, + COLORS: { + BG: '#020617', + SNAKE_HEAD: '#4ade80', + SNAKE_BODY: '#22c55e', + SNAKE_BODY_GRADIENT: '#166534', + APPLE: '#ef4444', + APPLE_SHINE: '#fca5a5', + WALL: '#475569', + GRID: 'rgba(56, 189, 248, 0.05)' + } +}; - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; +class Game { + constructor() { + this.canvas = document.getElementById('gameCanvas'); + this.ctx = this.canvas.getContext('2d'); + + this.winCount = parseInt(localStorage.getItem('winCount') || '0'); + this.bestScore = parseInt(localStorage.getItem('bestScore') || '0'); + this.lastWinTime = parseInt(localStorage.getItem('lastWinTime') || Date.now()); + + this.speedRange = document.getElementById('speedRange'); + this.winDisplay = document.getElementById('winCount'); + this.timerDisplay = document.getElementById('timer'); + this.scoreDisplay = document.getElementById('currentScore'); + this.bestScoreDisplay = document.getElementById('bestScore'); + this.resetBtn = document.getElementById('resetBtn'); + this.toggleMazeBtn = document.getElementById('toggleMazeBtn'); + this.overlay = document.getElementById('gameOverlay'); + + this.snake = []; + this.apple = { x: 0, y: 0 }; + this.mazeEnabled = false; + this.walls = []; + this.isGameOver = false; + this.isWin = false; + this.frameCount = 0; + + this.lastFrameTime = 0; + + this.init(); + this.setupEventListeners(); + this.reset(); + this.gameLoop(0); + } - appendMessage(message, 'visitor'); - chatInput.value = ''; + init() { + const resize = () => { + const container = this.canvas.parentElement; + const size = Math.min(container.clientWidth, 600); + this.canvas.width = size; + this.canvas.height = size; + CONFIG.TILE_SIZE = size / CONFIG.GRID_SIZE; + }; + window.addEventListener('resize', resize); + resize(); + + this.winDisplay.textContent = this.winCount; + this.bestScoreDisplay.textContent = this.bestScore; + this.updateTimer(); + setInterval(() => this.updateTimer(), 1000); + } - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); + setupEventListeners() { + this.resetBtn.addEventListener('click', () => { + this.winCount = 0; + this.bestScore = 0; + localStorage.setItem('winCount', '0'); + localStorage.setItem('bestScore', '0'); + this.winDisplay.textContent = '0'; + this.bestScoreDisplay.textContent = '0'; + this.reset(); + }); + + this.toggleMazeBtn.addEventListener('click', () => { + this.mazeEnabled = !this.mazeEnabled; + this.reset(); + }); + } + + reset() { + const mid = Math.floor(CONFIG.GRID_SIZE / 2); + this.snake = [ + { x: mid, y: mid }, + { x: mid, y: mid + 1 }, + { x: mid, y: mid + 2 } + ]; + this.isGameOver = false; + this.isWin = false; + this.overlay.classList.add('hidden'); + + this.generateMaze(); + this.spawnApple(); + this.scoreDisplay.textContent = '0'; + } + + generateMaze() { + this.walls = []; + if (!this.mazeEnabled) return; + + const padding = 4; + const length = 8; + for (let i = padding; i < padding + length; i++) { + this.walls.push({ x: i, y: padding }); + this.walls.push({ x: CONFIG.GRID_SIZE - 1 - i, y: CONFIG.GRID_SIZE - 1 - padding }); + this.walls.push({ x: padding, y: CONFIG.GRID_SIZE - 1 - i }); + this.walls.push({ x: CONFIG.GRID_SIZE - 1 - padding, y: i }); } - }); -}); + } + + spawnApple() { + let possible = []; + for (let y = 0; y < CONFIG.GRID_SIZE; y++) { + for (let x = 0; x < CONFIG.GRID_SIZE; x++) { + if (!this.isOccupied(x, y)) possible.push({ x, y }); + } + } + + if (possible.length === 0) { + this.handleWin(); + return; + } + this.apple = possible[Math.floor(Math.random() * possible.length)]; + } + + isOccupied(x, y, ignoreTail = false) { + if (x < 0 || x >= CONFIG.GRID_SIZE || y < 0 || y >= CONFIG.GRID_SIZE) return true; + const snakeToCheck = ignoreTail ? this.snake.slice(0, -1) : this.snake; + if (snakeToCheck.some(s => s.x === x && s.y === y)) return true; + if (this.walls.some(w => w.x === x && w.y === y)) return true; + return false; + } + + updateTimer() { + const diff = Math.floor((Date.now() - this.lastWinTime) / 1000); + const mins = Math.floor(diff / 60).toString().padStart(2, '0'); + const secs = (diff % 60).toString().padStart(2, '0'); + this.timerDisplay.textContent = `${mins}:${secs}`; + } + + handleWin() { + this.isWin = true; + this.winCount++; + this.lastWinTime = Date.now(); + localStorage.setItem('winCount', this.winCount); + localStorage.setItem('lastWinTime', this.lastWinTime); + this.winDisplay.textContent = this.winCount; + this.overlay.classList.remove('hidden'); + document.getElementById('overlayTitle').textContent = 'VICTORY!'; + document.getElementById('overlayMessage').textContent = 'Perfect cycle complete. Restarting...'; + setTimeout(() => this.reset(), 5000); + } + + handleGameOver() { + this.isGameOver = true; + this.overlay.classList.remove('hidden'); + document.getElementById('overlayTitle').textContent = 'RECALCULATING'; + document.getElementById('overlayMessage').textContent = 'Wait, adjusting strategy...'; + setTimeout(() => this.reset(), 1000); + } + + bfs(start, target, ignoreTail = false) { + const queue = [[start]]; + const visited = new Set(); + visited.add(`${start.x},${start.y}`); + + while (queue.length > 0) { + const path = queue.shift(); + const curr = path[path.length - 1]; + if (curr.x === target.x && curr.y === target.y) return path; + + const neighbors = [ + { x: curr.x, y: curr.y - 1 }, { x: curr.x, y: curr.y + 1 }, + { x: curr.x - 1, y: curr.y }, { x: curr.x + 1, y: curr.y } + ]; + + for (const n of neighbors) { + if (!this.isOccupied(n.x, n.y, ignoreTail) && !visited.has(`${n.x},${n.y}`)) { + visited.add(`${n.x},${n.y}`); + queue.push([...path, n]); + } + } + } + return null; + } + + getLongestPath(start, target) { + let path = this.bfs(start, target, true); + if (!path) return null; + + let extended = true; + while (extended) { + extended = false; + for (let i = 0; i < path.length - 1; i++) { + const p1 = path[i]; + const p2 = path[i+1]; + + const dirs = [ + { x: 0, y: -1 }, { x: 0, y: 1 }, + { x: -1, y: 0 }, { x: 1, y: 0 } + ]; + + for (const d of dirs) { + const n1 = { x: p1.x + d.x, y: p1.y + d.y }; + const n2 = { x: p2.x + d.x, y: p2.y + d.y }; + + if (!this.isOccupied(n1.x, n1.y, true) && !this.isOccupied(n2.x, n2.y, true) && + !path.some(p => p.x === n1.x && p.y === n1.y) && + !path.some(p => p.x === n2.x && p.y === n2.y)) { + + path.splice(i + 1, 0, n1, n2); + extended = true; + break; + } + } + if (extended) break; + } + } + return path; + } + + canReachTail(virtualSnake) { + if (virtualSnake.length < 2) return true; + const head = virtualSnake[0]; + const tail = virtualSnake[virtualSnake.length - 1]; + + const originalSnake = this.snake; + this.snake = virtualSnake; + const path = this.bfs(head, tail, true); + this.snake = originalSnake; + return !!path; + } + + update() { + if (this.isGameOver || this.isWin) return; + + const head = this.snake[0]; + const tail = this.snake[this.snake.length - 1]; + + // 1. Path to apple + let path = this.bfs(head, this.apple, true); + let nextMove = null; + + if (path && path.length > 1) { + const virtualSnake = [path[1], ...this.snake]; + if (!(path[1].x === this.apple.x && path[1].y === this.apple.y)) { + virtualSnake.pop(); + } + + if (this.canReachTail(virtualSnake)) { + nextMove = path[1]; + } + } + + // 2. Fallback: Longest path to tail + if (!nextMove) { + const pathToTail = this.getLongestPath(head, tail); + if (pathToTail && pathToTail.length > 1) { + nextMove = pathToTail[1]; + } else { + nextMove = this.getSurvivalMove(); + } + } + + if (!nextMove) { + this.handleGameOver(); + return; + } + + this.snake.unshift(nextMove); + if (nextMove.x === this.apple.x && nextMove.y === this.apple.y) { + const score = this.snake.length - 3; + this.scoreDisplay.textContent = score; + if (score > this.bestScore) { + this.bestScore = score; + this.bestScoreDisplay.textContent = score; + localStorage.setItem('bestScore', score); + } + this.spawnApple(); + } else { + this.snake.pop(); + } + this.frameCount++; + } + + getSurvivalMove() { + const head = this.snake[0]; + const neighbors = [ + { x: head.x, y: head.y - 1 }, { x: head.x, y: head.y + 1 }, + { x: head.x - 1, y: head.y }, { x: head.x + 1, y: head.y } + ].filter(n => !this.isOccupied(n.x, n.y)); + + if (neighbors.length === 0) return null; + return neighbors.sort((a, b) => this.getReachableArea(b) - this.getReachableArea(a))[0]; + } + + getReachableArea(pos) { + const visited = new Set(); + const stack = [pos]; + visited.add(`${pos.x},${pos.y}`); + let count = 0; + + while (stack.length > 0) { + const curr = stack.pop(); + count++; + + const neighbors = [ + { x: curr.x, y: curr.y - 1 }, { x: curr.x, y: curr.y + 1 }, + { x: curr.x - 1, y: curr.y }, { x: curr.x + 1, y: curr.y } + ]; + + for (const n of neighbors) { + if (!this.isOccupied(n.x, n.y) && !visited.has(`${n.x},${n.y}`)) { + visited.add(`${n.x},${n.y}`); + stack.push(n); + } + } + } + return count; + } + + draw() { + const { ctx, canvas } = this; + const ts = CONFIG.TILE_SIZE; + + ctx.fillStyle = CONFIG.COLORS.BG; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Grid + ctx.strokeStyle = CONFIG.COLORS.GRID; + ctx.lineWidth = 0.5; + for (let i = 0; i <= CONFIG.GRID_SIZE; i++) { + ctx.beginPath(); + ctx.moveTo(i * ts, 0); ctx.lineTo(i * ts, canvas.height); ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, i * ts); ctx.lineTo(canvas.width, i * ts); ctx.stroke(); + } + + // Walls + this.walls.forEach(w => { + const x = w.x * ts, y = w.y * ts; + ctx.fillStyle = CONFIG.COLORS.WALL; + this.drawTexturedRect(ctx, x, y, ts, ts, 'wall'); + }); + + // Apple + const ax = this.apple.x * ts + ts/2, ay = this.apple.y * ts + ts/2; + this.drawApple(ctx, ax, ay, ts); + + // Snake + for (let i = this.snake.length - 1; i >= 0; i--) { + const s = this.snake[i]; + const x = s.x * ts, y = s.y * ts; + if (i === 0) { + this.drawSnakeHead(ctx, x, y, ts); + } else if (i === this.snake.length - 1) { + this.drawSnakeTail(ctx, x, y, ts, i); + } else { + this.drawSnakeBody(ctx, x, y, ts, i); + } + } + } + + drawApple(ctx, x, y, ts) { + const radius = ts / 2 - 2; + ctx.fillStyle = 'rgba(0,0,0,0.3)'; + ctx.beginPath(); + ctx.ellipse(x, y + ts/4, radius, radius/2, 0, 0, Math.PI*2); + ctx.fill(); + + const grad = ctx.createRadialGradient(x - ts/6, y - ts/6, ts/10, x, y, ts/2); + grad.addColorStop(0, '#ff4d4d'); + grad.addColorStop(1, '#8b0000'); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = 'rgba(255,255,255,0.4)'; + ctx.beginPath(); + ctx.arc(x - ts/5, y - ts/5, ts/8, 0, Math.PI*2); + ctx.fill(); + + ctx.fillStyle = '#22c55e'; + ctx.beginPath(); + ctx.ellipse(x + 2, y - ts/2.5, ts/4, ts/8, Math.PI/4, 0, Math.PI * 2); + ctx.fill(); + } + + drawSnakeHead(ctx, x, y, ts) { + const next = this.snake[1]; + let rotation = 0; + if (next) { + if (next.x < this.snake[0].x) rotation = Math.PI / 2; + else if (next.x > this.snake[0].x) rotation = -Math.PI / 2; + else if (next.y < this.snake[0].y) rotation = Math.PI; + else if (next.y > this.snake[0].y) rotation = 0; + } + + ctx.save(); + ctx.translate(x + ts/2, y + ts/2); + ctx.rotate(rotation); + + const grad = ctx.createRadialGradient(0, 0, ts/4, 0, 0, ts/2); + grad.addColorStop(0, CONFIG.COLORS.SNAKE_HEAD); + grad.addColorStop(1, CONFIG.COLORS.SNAKE_BODY); + ctx.fillStyle = grad; + + this.drawRoundedRect(ctx, -ts/2 + 1, -ts/2 + 1, ts - 2, ts + 2, 12); + + // Apple tracking eyes + const dx = this.apple.x - this.snake[0].x; + const dy = this.apple.y - this.snake[0].y; + const angle = Math.atan2(dy, dx) - rotation + Math.PI/2; + const eyeOffset = ts * 0.05; + const ex = Math.cos(angle) * eyeOffset; + const ey = Math.sin(angle) * eyeOffset; + + ctx.fillStyle = '#fff'; + ctx.beginPath(); + ctx.arc(-ts*0.22, -ts*0.2, ts*0.14, 0, Math.PI*2); + ctx.arc(ts*0.22, -ts*0.2, ts*0.14, 0, Math.PI*2); + ctx.fill(); + + ctx.fillStyle = '#000'; + ctx.beginPath(); + ctx.arc(-ts*0.22 + ex, -ts*0.2 + ey, ts*0.07, 0, Math.PI*2); + ctx.arc(ts*0.22 + ex, -ts*0.2 + ey, ts*0.07, 0, Math.PI*2); + ctx.fill(); + + // Nostrils + ctx.fillStyle = 'rgba(0,0,0,0.3)'; + ctx.beginPath(); + ctx.arc(-ts*0.1, -ts*0.4, 2, 0, Math.PI*2); + ctx.arc(ts*0.1, -ts*0.4, 2, 0, Math.PI*2); + ctx.fill(); + + if (this.frameCount % 20 < 10) { + ctx.strokeStyle = '#ef4444'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(0, -ts/2); + ctx.lineTo(0, -ts/2 - 10); + ctx.lineTo(-4, -ts/2 - 14); + ctx.moveTo(0, -ts/2 - 10); + ctx.lineTo(4, -ts/2 - 14); + ctx.stroke(); + } + + ctx.restore(); + } + + drawSnakeBody(ctx, x, y, ts, index) { + const curr = this.snake[index]; + const next = this.snake[index - 1]; + const prev = this.snake[index + 1]; + + if (!next || !prev) return; + + ctx.save(); + ctx.translate(x + ts/2, y + ts/2); + + const grad = ctx.createLinearGradient(-ts/2, -ts/2, ts/2, ts/2); + grad.addColorStop(0, CONFIG.COLORS.SNAKE_BODY); + grad.addColorStop(1, CONFIG.COLORS.SNAKE_BODY_GRADIENT); + ctx.fillStyle = grad; + + const dirNext = { x: next.x - curr.x, y: next.y - curr.y }; + const dirPrev = { x: prev.x - curr.x, y: prev.y - curr.y }; + + const isHorizontal = next.y === prev.y; + const isVertical = next.x === prev.x; + + if (isHorizontal) { + this.drawRoundedRect(ctx, -ts/2, -ts/2 + 2, ts, ts - 4, 4); + } else if (isVertical) { + this.drawRoundedRect(ctx, -ts/2 + 2, -ts/2, ts - 4, ts, 4); + } else { + // Corner - smooth arc connection + ctx.beginPath(); + if ((dirNext.x === 1 && dirPrev.y === 1) || (dirNext.y === 1 && dirPrev.x === 1)) { + // Bottom-right + ctx.arc(ts/2, ts/2, ts - 2, Math.PI, Math.PI * 1.5); + ctx.lineTo(ts/2, ts/2); + } else if ((dirNext.x === -1 && dirPrev.y === 1) || (dirNext.y === 1 && dirPrev.x === -1)) { + // Bottom-left + ctx.arc(-ts/2, ts/2, ts - 2, Math.PI * 1.5, 0); + ctx.lineTo(-ts/2, ts/2); + } else if ((dirNext.x === 1 && dirPrev.y === -1) || (dirNext.y === -1 && dirPrev.x === 1)) { + // Top-right + ctx.arc(ts/2, -ts/2, ts - 2, Math.PI * 0.5, Math.PI); + ctx.lineTo(ts/2, -ts/2); + } else { + // Top-left + ctx.arc(-ts/2, -ts/2, ts - 2, 0, Math.PI * 0.5); + ctx.lineTo(-ts/2, -ts/2); + } + ctx.closePath(); + ctx.fill(); + } + + // Animated scales + if ((index + Math.floor(this.frameCount/2)) % 6 === 0) { + ctx.fillStyle = 'rgba(255,255,255,0.12)'; + ctx.beginPath(); + ctx.arc(0, 0, ts/5, 0, Math.PI * 2); + ctx.fill(); + } + + ctx.restore(); + } + + drawSnakeTail(ctx, x, y, ts, index) { + const curr = this.snake[index]; + const prev = this.snake[index - 1]; + if (!prev) return; + + let rotation = 0; + if (prev.x < curr.x) rotation = Math.PI / 2; + else if (prev.x > curr.x) rotation = -Math.PI / 2; + else if (prev.y < curr.y) rotation = Math.PI; + else if (prev.y > curr.y) rotation = 0; + + ctx.save(); + ctx.translate(x + ts/2, y + ts/2); + ctx.rotate(rotation); + + const grad = ctx.createLinearGradient(-ts/2, -ts/2, ts/2, ts/2); + grad.addColorStop(0, CONFIG.COLORS.SNAKE_BODY); + grad.addColorStop(1, CONFIG.COLORS.SNAKE_BODY_GRADIENT); + ctx.fillStyle = grad; + + // Tapered tail + ctx.beginPath(); + ctx.moveTo(-ts/2 + 4, -ts/2); + ctx.lineTo(ts/2 - 4, -ts/2); + ctx.quadraticCurveTo(0, ts/2, 0, ts/2 + 6); + ctx.closePath(); + ctx.fill(); + + ctx.restore(); + } + + drawTexturedRect(ctx, x, y, w, h, type) { + if (type === 'wall') { + const grad = ctx.createLinearGradient(x, y, x + w, y + h); + grad.addColorStop(0, '#475569'); + grad.addColorStop(1, '#1e293b'); + ctx.fillStyle = grad; + this.drawRoundedRect(ctx, x + 2, y + 2, w - 4, h - 4, 6); + + ctx.strokeStyle = 'rgba(255,255,255,0.1)'; + ctx.strokeRect(x + 6, y + 6, w - 12, h - 12); + } + } + + drawRoundedRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + if (ctx.roundRect) { + ctx.roundRect(x, y, w, h, r); + } else { + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + r); + ctx.lineTo(x + w, y + h - r); + ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + ctx.lineTo(x + r, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - r); + ctx.lineTo(x, y + r); + ctx.quadraticCurveTo(x, y, x + r, y); + } + ctx.closePath(); + ctx.fill(); + } + + gameLoop(timestamp) { + const speed = parseInt(this.speedRange.value); + const interval = 1000 / speed; + + if (timestamp - this.lastFrameTime >= interval) { + this.update(); + this.draw(); + this.lastFrameTime = timestamp; + } + + requestAnimationFrame((t) => this.gameLoop(t)); + } +} + +document.addEventListener('DOMContentLoaded', () => new Game()); \ No newline at end of file diff --git a/assets/pasted-20260303-004347-83d8b12e.jpg b/assets/pasted-20260303-004347-83d8b12e.jpg new file mode 100644 index 0000000..7f9cca0 Binary files /dev/null and b/assets/pasted-20260303-004347-83d8b12e.jpg differ diff --git a/assets/pasted-20260303-004846-1f6b05b6.jpg b/assets/pasted-20260303-004846-1f6b05b6.jpg new file mode 100644 index 0000000..32462b8 Binary files /dev/null and b/assets/pasted-20260303-004846-1f6b05b6.jpg differ diff --git a/assets/pasted-20260303-005216-02bf6ec0.jpg b/assets/pasted-20260303-005216-02bf6ec0.jpg new file mode 100644 index 0000000..a42a752 Binary files /dev/null and b/assets/pasted-20260303-005216-02bf6ec0.jpg differ diff --git a/assets/pasted-20260303-005641-21839e8c.jpg b/assets/pasted-20260303-005641-21839e8c.jpg new file mode 100644 index 0000000..76ed4a7 Binary files /dev/null and b/assets/pasted-20260303-005641-21839e8c.jpg differ diff --git a/assets/pasted-20260303-011135-6f2f6c05.jpg b/assets/pasted-20260303-011135-6f2f6c05.jpg new file mode 100644 index 0000000..0cd6a78 Binary files /dev/null and b/assets/pasted-20260303-011135-6f2f6c05.jpg differ diff --git a/index.php b/index.php index 7205f3d..e021ecf 100644 --- a/index.php +++ b/index.php @@ -6,145 +6,118 @@ declare(strict_types=1); $phpVersion = PHP_VERSION; $now = date('Y-m-d H:i:s'); + +// SEO Meta Tags +$title = "Auto-Snake: The Infinite Loop"; +$description = "Watch an AI-powered snake navigate mazes and eat apples in an infinite, perfect loop. A mesmerizing demonstration of pathfinding."; +$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; ?> - - New Style - - - - - - - - - - - - - - - + + <?= htmlspecialchars($title) ?> + + + + + + + + + + + + + + + + + + - - + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
+ +
+
+

Auto-Snake AI Bot

+

Smart, smooth, and never-ending.

+
+ +
+
+
+ + +
+
+ +
-