Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Matrix Snake 3D - Enhanced</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background-color: #000; | |
| color: #0f0; | |
| font-family: 'Courier New', Courier, monospace; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| .game-ui { | |
| position: absolute; | |
| padding: 10px; | |
| background-color: rgba(0, 20, 0, 0.8); | |
| border: 1px solid #0f0; | |
| border-radius: 5px; | |
| font-size: 1.2em; | |
| pointer-events: none; | |
| } | |
| #info { | |
| top: 10px; | |
| left: 10px; | |
| } | |
| #combo { | |
| top: 10px; | |
| right: 10px; | |
| color: #0ff; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| #gameScreen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background-color: rgba(0, 10, 0, 0.8); | |
| z-index: 10; | |
| } | |
| #startScreen, #gameOverScreen { | |
| padding: 30px; | |
| background-color: rgba(0, 30, 0, 0.9); | |
| border: 2px solid #0f0; | |
| border-radius: 10px; | |
| text-align: center; | |
| max-width: 500px; | |
| } | |
| #gameOverScreen { | |
| border-color: #f00; | |
| } | |
| .title { | |
| font-size: 2.5em; | |
| margin-bottom: 20px; | |
| text-shadow: 0 0 10px #0f0; | |
| } | |
| .subtitle { | |
| font-size: 1.2em; | |
| margin-bottom: 30px; | |
| } | |
| .button { | |
| display: inline-block; | |
| padding: 10px 20px; | |
| margin: 10px; | |
| background-color: rgba(0, 80, 0, 0.8); | |
| border: 1px solid #0f0; | |
| border-radius: 5px; | |
| color: #0f0; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| pointer-events: auto; | |
| } | |
| .button:hover { | |
| background-color: rgba(0, 120, 0, 0.9); | |
| transform: scale(1.05); | |
| } | |
| .controls { | |
| margin-top: 20px; | |
| font-size: 0.9em; | |
| opacity: 0.8; | |
| } | |
| #highScores { | |
| margin-top: 20px; | |
| text-align: left; | |
| width: 100%; | |
| } | |
| #highScores table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| #highScores th, #highScores td { | |
| padding: 5px; | |
| border-bottom: 1px solid rgba(0, 255, 0, 0.5); | |
| } | |
| #touchControls { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: none; /* Hidden by default, shown on mobile */ | |
| } | |
| .touchBtn { | |
| width: 60px; | |
| height: 60px; | |
| background-color: rgba(0, 50, 0, 0.5); | |
| border: 1px solid #0f0; | |
| border-radius: 50%; | |
| margin: 5px; | |
| display: inline-flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-size: 20px; | |
| cursor: pointer; | |
| pointer-events: auto; | |
| } | |
| /* Matrix animation background */ | |
| #matrixCanvas { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| z-index: -1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Matrix background --> | |
| <canvas id="matrixCanvas"></canvas> | |
| <!-- Game canvas --> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- Game UI --> | |
| <div id="info" class="game-ui">Score: 0 | High: 0</div> | |
| <div id="combo" class="game-ui">Combo x1!</div> | |
| <!-- Touch controls for mobile --> | |
| <div id="touchControls"> | |
| <div class="touchBtn" id="upBtn">↑</div> | |
| <div style="display: flex;"> | |
| <div class="touchBtn" id="leftBtn">←</div> | |
| <div class="touchBtn" id="downBtn">↓</div> | |
| <div class="touchBtn" id="rightBtn">→</div> | |
| </div> | |
| </div> | |
| <!-- Game screens --> | |
| <div id="gameScreen"> | |
| <div id="startScreen"> | |
| <div class="title">MATRIX SNAKE 3D</div> | |
| <div class="subtitle">Navigate the digital realm. Collect data packets. Avoid system firewalls.</div> | |
| <div class="button" id="startBtn">START GAME</div> | |
| <div class="button" id="difficultyBtn">DIFFICULTY: NORMAL</div> | |
| <div class="controls"> | |
| Use Arrow Keys to change direction<br> | |
| Press P to pause the game | |
| </div> | |
| <div id="highScores"> | |
| <h3>HIGH SCORES</h3> | |
| <table id="scoresTable"> | |
| <tr><th>RANK</th><th>SCORE</th><th>DIFFICULTY</th></tr> | |
| </table> | |
| </div> | |
| </div> | |
| <div id="gameOverScreen" style="display: none;"> | |
| <div class="title" style="color: #f00;">SYSTEM FAILURE</div> | |
| <div id="finalScore" class="subtitle">Final Score: 0</div> | |
| <div class="button" id="restartBtn">RESTART</div> | |
| <div class="button" id="menuBtn">MAIN MENU</div> | |
| </div> | |
| <div id="pauseScreen" style="display: none;"> | |
| <div class="title">PAUSED</div> | |
| <div class="subtitle">Press P to resume</div> | |
| <div class="button" id="resumeBtn">RESUME</div> | |
| <div class="button" id="quitBtn">QUIT</div> | |
| </div> | |
| </div> | |
| <!-- Audio elements --> | |
| <audio id="eatSound" preload="auto"> | |
| <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAANIAqJWUEQAFO+gRc5TRJIkiRJEiL///////////8RERERERERVVVVVVVVVVVVVVJEREREREVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/jGMQJA/Aa1flBABBTpGX9hDGMYxw7/+MMYxd/4wxIiI9////jDEQ7/jdEiJERERBaIiIzMzMzIiIiP//MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM/+MYxB4AAANIAAAAADMzMzMzMzMzMzMzMzMzMzMzM" type="audio/mpeg"> | |
| </audio> | |
| <audio id="gameOverSound" preload="auto"> | |
| <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAKMFqZVQEwAhGKzc+FSIiIiIiIiIj4+Pj4+Pj4+Pj4+JIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIv/jIMQNAAAP8AEAAAI+IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIv/jEMQQAAAP8AAAAAI+IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIg==" type="audio/mpeg"> | |
| </audio> | |
| <audio id="bgMusic" loop preload="auto"> | |
| <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAJcAKRWQEQAFNfQRc5znOc5znP/////////uc5znOc5znOc5znEREREREREREREMYxjGMY/+MYxBEJkFahX4wwAjGMYxjGMYxjGMYxERERERERESIiIiL//////////////+MYxBQG4AqlX8MQAu/////////////////////jIMQVBVwCqVfwBAC/////////////////" type="audio/mpeg"> | |
| </audio> | |
| // <!-- <script type="importmap"> | |
| // { | |
| // "imports": { | |
| "three": "https://unpkg.com/three@0.163.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/" | |
| // } | |
| // } | |
| // </script> --> | |
| <script type="module"> | |
| import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js'; | |
| import { EffectComposer } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js'; | |
| import { RenderPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/RenderPass.js'; | |
| import { UnrealBloomPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/UnrealBloomPass.js'; | |
| // Game configuration | |
| const CONFIG = { | |
| GRID_SIZE: 25, // Number of units across/deep | |
| CELL_SIZE: 1, // Size of each grid cell/snake segment | |
| BASE_SPEED: 150, // Base milliseconds between updates | |
| DIFFICULTY_LEVELS: { | |
| 'EASY': { speedMultiplier: 1.3, obstacleMultiplier: 0.5 }, | |
| 'NORMAL': { speedMultiplier: 1.0, obstacleMultiplier: 1.0 }, | |
| 'HARD': { speedMultiplier: 0.7, obstacleMultiplier: 1.5 } | |
| }, | |
| MAX_OBSTACLE_COUNT: 10, // Maximum number of obstacles | |
| FOOD_TYPES: [ | |
| { type: 'regular', color: 0x00ff00, points: 1, speedEffect: 0 }, | |
| { type: 'special', color: 0x00ffff, points: 5, speedEffect: -10 }, | |
| { type: 'rare', color: 0xff00ff, points: 10, speedEffect: 10 } | |
| ], | |
| COMBO_TIMEOUT: 5000, // Milliseconds to get next food for combo | |
| HIGH_SCORES_COUNT: 5 // Number of high scores to save | |
| }; | |
| // --- Particle System for Effects --- | |
| class ParticleSystem { | |
| constructor(scene) { | |
| this.scene = scene; | |
| this.particles = []; | |
| // Shared geometry for all particles | |
| this.geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2); | |
| } | |
| createFoodEffect(position, color) { | |
| const count = 20; // Number of particles | |
| for (let i = 0; i < count; i++) { | |
| // Create particle | |
| const material = new THREE.MeshBasicMaterial({ | |
| color: color || 0x00ff00, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const particle = new THREE.Mesh(this.geometry, material); | |
| // Set initial position | |
| particle.position.copy(position); | |
| // Set random velocity | |
| const velocity = new THREE.Vector3( | |
| (Math.random() - 0.5) * 0.1, | |
| (Math.random()) * 0.1, | |
| (Math.random() - 0.5) * 0.1 | |
| ); | |
| // Add to scene | |
| this.scene.add(particle); | |
| // Store particle data | |
| this.particles.push({ | |
| mesh: particle, | |
| velocity: velocity, | |
| life: 1.0, // Life from 1.0 to 0.0 | |
| decay: 0.02 + Math.random() * 0.03 // Random decay rate | |
| }); | |
| } | |
| } | |
| update() { | |
| // Update all particles | |
| for (let i = this.particles.length - 1; i >= 0; i--) { | |
| const particle = this.particles[i]; | |
| // Update position | |
| particle.mesh.position.add(particle.velocity); | |
| // Simulate gravity | |
| particle.velocity.y -= 0.003; | |
| // Update life | |
| particle.life -= particle.decay; | |
| // Update opacity based on life | |
| particle.mesh.material.opacity = particle.life; | |
| // Remove dead particles | |
| if (particle.life <= 0) { | |
| this.scene.remove(particle.mesh); | |
| particle.mesh.material.dispose(); | |
| this.particles.splice(i, 1); | |
| } | |
| } | |
| } | |
| clear() { | |
| // Remove all particles | |
| for (const particle of this.particles) { | |
| this.scene.remove(particle.mesh); | |
| particle.mesh.material.dispose(); | |
| particle.mesh.geometry.dispose(); | |
| } | |
| this.particles = []; | |
| } | |
| } | |
| // Game state management | |
| const GameState = { | |
| MENU: 'menu', | |
| PLAYING: 'playing', | |
| PAUSED: 'paused', | |
| GAME_OVER: 'gameOver', | |
| currentState: 'menu', | |
| changeState(newState) { | |
| this.currentState = newState; | |
| // Handle UI changes based on state | |
| switch(newState) { | |
| case this.MENU: | |
| document.getElementById('gameScreen').style.display = 'flex'; | |
| document.getElementById('startScreen').style.display = 'block'; | |
| document.getElementById('gameOverScreen').style.display = 'none'; | |
| break; | |
| case this.PLAYING: | |
| document.getElementById('gameScreen').style.display = 'none'; | |
| break; | |
| case this.PAUSED: | |
| document.getElementById('gameScreen').style.display = 'flex'; | |
| document.getElementById('startScreen').style.display = 'none'; | |
| document.getElementById('gameOverScreen').style.display = 'none'; | |
| document.getElementById('pauseScreen').style.display = 'block'; | |
| break; | |
| case this.GAME_OVER: | |
| document.getElementById('gameScreen').style.display = 'flex'; | |
| document.getElementById('startScreen').style.display = 'none'; | |
| document.getElementById('gameOverScreen').style.display = 'block'; | |
| // The score will be updated by the game instance when it triggers game over | |
| document.getElementById('gameOverSound').play(); | |
| break; | |
| } | |
| } | |
| }; | |
| // --- Matrix Rain Background Effect --- | |
| class MatrixRain { | |
| constructor() { | |
| this.canvas = document.getElementById('matrixCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.resize(); | |
| this.fontSize = 14; | |
| this.columns = Math.floor(this.canvas.width / this.fontSize); | |
| this.drops = []; | |
| this.characters = '01アイウエオカキクケコサシスセソタチツテトナニヌネ<>{}[]()+-*/%=#@&?*:・゚✧ ≡ ░▒░▒░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█䷀ ▙⁞ ░▒▓█║│ ·▓▒░█▄▀■■▄▬▌▐ ⁞▏▄▀■■▄▬▌▐ ▄▀■■▄▬▌▐ . ▛ ⁞▏ ▏ ⁚⁝ .'; | |
| this.resetDrops(); | |
| this.animate = this.animate.bind(this); | |
| this.animate(); | |
| window.addEventListener('resize', this.handleResize.bind(this)); | |
| } | |
| handleResize() { | |
| this.resize(); | |
| this.columns = Math.floor(this.canvas.width / this.fontSize); | |
| this.resetDrops(); | |
| } | |
| resize() { | |
| this.canvas.width = window.innerWidth; | |
| this.canvas.height = window.innerHeight; | |
| } | |
| resetDrops() { | |
| this.drops = []; | |
| for(let i = 0; i < this.columns; i++) { | |
| // Start drops at random negative positions for staggered effect | |
| this.drops[i] = Math.floor(Math.random() * -100); | |
| } | |
| } | |
| animate() { | |
| if (GameState.currentState === GameState.PLAYING) { | |
| // Semi-transparent background to create fade effect | |
| this.ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| this.ctx.fillStyle = '#0f0'; | |
| this.ctx.font = this.fontSize + 'px monospace'; | |
| for(let i = 0; i < this.drops.length; i++) { | |
| // Choose a random character | |
| const text = this.characters.charAt(Math.floor(Math.random() * this.characters.length)); | |
| // Draw the character | |
| this.ctx.fillText(text, i * this.fontSize, this.drops[i] * this.fontSize); | |
| // Move drops down and reset when off the screen | |
| if(this.drops[i] * this.fontSize > this.canvas.height && Math.random() > 0.975) { | |
| this.drops[i] = 0; | |
| } | |
| this.drops[i]++; | |
| } | |
| } | |
| requestAnimationFrame(this.animate); | |
| } | |
| } | |
| // --- Object pooling for performance optimization --- | |
| class ObjectPool { | |
| constructor(createFunc, initialCount = 10) { | |
| this.pool = []; | |
| this.createFunc = createFunc; | |
| // Populate the pool initially | |
| for (let i = 0; i < initialCount; i++) { | |
| this.pool.push(this.createFunc()); | |
| } | |
| } | |
| get() { | |
| if (this.pool.length > 0) { | |
| return this.pool.pop(); | |
| } | |
| return this.createFunc(); | |
| } | |
| release(object) { | |
| this.pool.push(object); | |
| } | |
| clear() { | |
| this.pool = []; | |
| } | |
| } | |
| // --- Main Game Class --- | |
| class SnakeGame { | |
| constructor() { | |
| // Initialize properties | |
| this.scene = null; | |
| this.camera = null; | |
| this.renderer = null; | |
| this.snake = []; | |
| this.food = null; | |
| this.obstacles = []; | |
| this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0); | |
| this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0); | |
| this.score = 0; | |
| this.highScore = this.loadHighScores()[0]?.score || 0; | |
| this.gameSpeed = CONFIG.BASE_SPEED; | |
| this.lastUpdateTime = 0; | |
| this.isGameOver = false; | |
| this.isPaused = false; | |
| this.gameLoopId = null; | |
| this.bounds = Math.floor(CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE; | |
| this.obstacleCount = CONFIG.MAX_OBSTACLE_COUNT; | |
| this.comboCount = 0; | |
| this.lastFoodTime = 0; | |
| this.currentDifficulty = 'NORMAL'; | |
| this.particleSystem = null; | |
| this.headLight = null; | |
| // Initialize materials | |
| this.materials = { | |
| snakeHead: new THREE.MeshStandardMaterial({ | |
| color: 0x0000ff, | |
| emissive: 0x39FF14, | |
| roughness: 0.8, | |
| metalness: 0.22 | |
| }), | |
| snakeBody: new THREE.MeshStandardMaterial({ | |
| color: 0x00ff00, | |
| emissive: 0x005500, | |
| roughness: 0.3, | |
| metalness: 0.72 | |
| }), | |
| food: new THREE.MeshBasicMaterial({ | |
| color: 0x00ff00, | |
| wireframe: true | |
| }), | |
| obstacle: new THREE.MeshBasicMaterial({ | |
| color: 0x008800, | |
| wireframe: true | |
| }), | |
| specialFood: new THREE.MeshBasicMaterial({ | |
| color: 0x00ffff, | |
| wireframe: true | |
| }), | |
| rareFood: new THREE.MeshBasicMaterial({ | |
| color: 0xff00ff, | |
| wireframe: true | |
| }) | |
| }; | |
| // Initialize geometries | |
| this.geometries = { | |
| segment: new THREE.BoxGeometry( | |
| CONFIG.CELL_SIZE, | |
| CONFIG.CELL_SIZE, | |
| CONFIG.CELL_SIZE | |
| ), | |
| foodBox: new THREE.BoxGeometry( | |
| CONFIG.CELL_SIZE * 0.8, | |
| CONFIG.CELL_SIZE * 0.8, | |
| CONFIG.CELL_SIZE * 0.8 | |
| ), | |
| foodSphere: new THREE.SphereGeometry( | |
| CONFIG.CELL_SIZE * 0.5, | |
| 16, | |
| 12 | |
| ), | |
| foodTetrahedron: new THREE.TetrahedronGeometry( | |
| CONFIG.CELL_SIZE * 0.6, | |
| 0 | |
| ), | |
| obstacle: new THREE.BoxGeometry( | |
| CONFIG.CELL_SIZE, | |
| CONFIG.CELL_SIZE * 1.5, | |
| CONFIG.CELL_SIZE | |
| ) | |
| }; | |
| // Initialize object pools | |
| this.segmentPool = new ObjectPool(() => { | |
| return new THREE.Mesh(this.geometries.segment, this.materials.snakeBody.clone()); | |
| }, 20); | |
| this.obstaclePool = new ObjectPool(() => { | |
| return new THREE.Mesh(this.geometries.obstacle, this.materials.obstacle); | |
| }, CONFIG.MAX_OBSTACLE_COUNT * 1.5); | |
| // Initialize the game | |
| this.setupEventListeners(); | |
| this.init(); | |
| // Create the matrix rain effect | |
| this.matrixRain = new MatrixRain(); | |
| // Update high scores display | |
| this.updateHighScoresTable(); | |
| } | |
| // Place food at random position | |
| placeFood() { | |
| let foodPos; | |
| let validPosition = false; | |
| let attempts = 0; | |
| const maxAttempts = 100; // Prevent infinite loop | |
| while (!validPosition && attempts < maxAttempts) { | |
| foodPos = new THREE.Vector3( | |
| Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE, | |
| 0, | |
| Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE | |
| ); | |
| // Check collision with snake | |
| let collisionWithSnake = this.snake.some(segment => | |
| segment.position.distanceTo(foodPos) < CONFIG.CELL_SIZE * 0.9 | |
| ); | |
| // Check collision with obstacles | |
| let collisionWithObstacle = this.obstacles.some(obstacle => | |
| obstacle.position.distanceTo(foodPos) < CONFIG.CELL_SIZE * 0.9 | |
| ); | |
| validPosition = !collisionWithSnake && !collisionWithObstacle; | |
| attempts++; | |
| } | |
| if (validPosition) { | |
| this.food.position.copy(foodPos); | |
| } else { | |
| // Fallback in case we can't find a position after max attempts | |
| console.warn("Could not find valid position for food after max attempts"); | |
| this.food.position.set(0, 5, 0); // Place above play area | |
| } | |
| } | |
| // Create obstacles | |
| createObstacles() { | |
| // Clear existing obstacles | |
| for (const obstacle of this.obstacles) { | |
| this.scene.remove(obstacle); | |
| } | |
| this.obstacles = []; | |
| // Create new obstacles | |
| for (let i = 0; i < this.obstacleCount; i++) { | |
| let obstaclePos; | |
| let validPosition = false; | |
| let attempts = 0; | |
| const maxAttempts = 50; | |
| while (!validPosition && attempts < maxAttempts) { | |
| obstaclePos = new THREE.Vector3( | |
| Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE, | |
| 0, | |
| Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE | |
| ); | |
| // Check distance from snake start position | |
| let tooCloseToStart = obstaclePos.length() < CONFIG.CELL_SIZE * 3; | |
| // Check collision with snake and other obstacles | |
| let collisionWithSnake = this.snake.some(segment => | |
| segment.position.distanceTo(obstaclePos) < CONFIG.CELL_SIZE * 2 | |
| ); | |
| let collisionWithObstacle = this.obstacles.some(obstacle => | |
| obstacle.position.distanceTo(obstaclePos) < CONFIG.CELL_SIZE | |
| ); | |
| validPosition = !tooCloseToStart && !collisionWithSnake && !collisionWithObstacle; | |
| attempts++; | |
| } | |
| if (validPosition) { | |
| const obstacle = new THREE.Mesh(this.geometries.segment, this.materials.obstacle); | |
| obstacle.position.copy(obstaclePos); | |
| this.obstacles.push(obstacle); | |
| this.scene.add(obstacle); | |
| } | |
| } | |
| } | |
| // Clear all game objects for a new game | |
| clearGameObjects() { | |
| // Clear snake | |
| for (const segment of this.snake) { | |
| this.scene.remove(segment); | |
| this.segmentPool.release(segment); | |
| } | |
| this.snake = []; | |
| // Clear food | |
| if (this.food) { | |
| this.scene.remove(this.food); | |
| this.food = null; | |
| } | |
| // Clear obstacles | |
| for (const obstacle of this.obstacles) { | |
| this.scene.remove(obstacle); | |
| } | |
| this.obstacles = []; | |
| // Clear particles | |
| this.particleSystem.clear(); | |
| } | |
| // Update game logic | |
| update(time) { | |
| // Skip update if game is paused, over, or not playing | |
| if (this.isPaused || this.isGameOver || GameState.currentState !== GameState.PLAYING) { | |
| return; | |
| } | |
| // Control game speed | |
| if (time - this.lastUpdateTime < this.gameSpeed) { | |
| return; | |
| } | |
| this.lastUpdateTime = time; | |
| // Update direction safely | |
| this.direction = this.nextDirection.clone(); | |
| // Get current head position | |
| const head = this.snake[0]; | |
| const newHeadPos = head.position.clone().add( | |
| this.direction.clone().multiplyScalar(CONFIG.CELL_SIZE) | |
| ); | |
| // Check collision with walls | |
| const halfGrid = CONFIG.GRID_SIZE / 2; | |
| if ( | |
| newHeadPos.x > halfGrid * CONFIG.CELL_SIZE || | |
| newHeadPos.x < -halfGrid * CONFIG.CELL_SIZE || | |
| newHeadPos.z > halfGrid * CONFIG.CELL_SIZE || | |
| newHeadPos.z < -halfGrid * CONFIG.CELL_SIZE | |
| ) { | |
| this.triggerGameOver(); | |
| return; | |
| } | |
| // Check collision with self | |
| for (let i = 1; i < this.snake.length; i++) { | |
| if (newHeadPos.distanceTo(this.snake[i].position) < CONFIG.CELL_SIZE * 0.25) { | |
| this.triggerGameOver(); | |
| return; | |
| } | |
| } | |
| // Check collision with obstacles | |
| for (const obstacle of this.obstacles) { | |
| if (newHeadPos.distanceTo(obstacle.position) < CONFIG.CELL_SIZE * 0.5) { | |
| this.triggerGameOver(); | |
| return; | |
| } | |
| } | |
| // Create new head segment | |
| const newHead = this.segmentPool.get(); | |
| newHead.position.copy(newHeadPos); | |
| this.snake.unshift(newHead); | |
| this.scene.add(newHead); | |
| // Check for food collision | |
| if (this.food && newHeadPos.distanceTo(this.food.position) < CONFIG.CELL_SIZE * 0.5) { | |
| // Get food properties | |
| const foodType = this.food.userData; | |
| // Increase score | |
| const basePoints = foodType.points || 1; | |
| // Handle combo system | |
| const currentTime = performance.now(); | |
| if (currentTime - this.lastFoodTime < CONFIG.COMBO_TIMEOUT) { | |
| this.comboCount++; | |
| } else { | |
| this.comboCount = 1; | |
| } | |
| this.lastFoodTime = currentTime; | |
| // Calculate final score with combo multiplier | |
| const points = basePoints * this.comboCount; | |
| this.score += points; | |
| // Show combo | |
| if (this.comboCount > 1) { | |
| const comboElement = document.getElementById('combo'); | |
| comboElement.textContent = `Combo x${this.comboCount}! +${points}`; | |
| comboElement.style.opacity = 1; | |
| // Hide combo text after a delay | |
| setTimeout(() => { | |
| comboElement.style.opacity = 0; | |
| }, 2000); | |
| } | |
| // Update score display | |
| document.getElementById('info').textContent = `Score: ${this.score} | High: ${Math.max(this.score, this.highScore)}`; | |
| // Apply speed effect from food type | |
| if (foodType.speedEffect) { | |
| this.gameSpeed = Math.max(50, this.gameSpeed - foodType.speedEffect); | |
| } | |
| // Play eat sound | |
| document.getElementById('eatSound').play(); | |
| // Create particle effect at food position | |
| this.particleSystem.createFoodEffect(this.food.position.clone(), foodType.color); | |
| // Place new food | |
| this.chooseFoodType(); | |
| this.placeFood(); | |
| } else { | |
| // Remove tail if not eating | |
| const tail = this.snake.pop(); | |
| this.scene.remove(tail); | |
| this.segmentPool.release(tail); | |
| } | |
| // Update particles | |
| this.particleSystem.update(); | |
| // Animate snake segments (subtle wave effect) | |
| for (let i = 0; i < this.snake.length; i++) { | |
| const segment = this.snake[i]; | |
| segment.rotation.y = Math.sin(time * 0.001 + i * 0.2) * 0.1; | |
| segment.position.y = Math.sin(time * 0.002 + i * 0.1) * 0.2; | |
| } | |
| // Animate food | |
| if (this.food) { | |
| this.food.rotation.y += 0.05; | |
| this.food.position.y = Math.sin(time * 0.002) * 0.3; | |
| } | |
| } | |
| // Choose a food type based on probability | |
| chooseFoodType() { | |
| // Food type probability | |
| const rand = Math.random(); | |
| let foodType; | |
| if (rand < 0.05) { // 5% chance for rare food | |
| foodType = CONFIG.FOOD_TYPES[2]; | |
| } else if (rand < 0.25) { // 20% chance for special food | |
| foodType = CONFIG.FOOD_TYPES[1]; | |
| } else { // 75% chance for regular food | |
| foodType = CONFIG.FOOD_TYPES[0]; | |
| } | |
| // Create food mesh with appropriate material | |
| let material; | |
| switch(foodType.type) { | |
| case 'special': | |
| material = this.materials.specialFood; | |
| break; | |
| case 'rare': | |
| material = this.materials.rareFood; | |
| break; | |
| default: | |
| material = this.materials.food; | |
| } | |
| // Create or update food mesh | |
| if (!this.food) { | |
| this.food = new THREE.Mesh( | |
| this.geometries.segment, | |
| material | |
| ); | |
| this.scene.add(this.food); | |
| } else { | |
| this.food.material = material; | |
| } | |
| // Store food type data | |
| this.food.userData = foodType; | |
| } | |
| // Reset the game | |
| resetGame() { | |
| // Clear all game objects | |
| this.clearGameObjects(); | |
| // Stop any playing audio | |
| document.getElementById('eatSound').pause(); | |
| document.getElementById('gameOverSound').pause(); | |
| document.getElementById('bgMusic').pause(); | |
| // Reset game state | |
| this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0); | |
| this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0); | |
| this.score = 0; | |
| this.gameSpeed = CONFIG.BASE_SPEED * CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].speedMultiplier; | |
| this.isGameOver = false; | |
| this.isPaused = false; | |
| this.comboCount = 0; | |
| this.lastFoodTime = 0; | |
| // Update obstacle count based on difficulty | |
| this.obstacleCount = Math.floor(CONFIG.MAX_OBSTACLE_COUNT * | |
| CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].obstacleMultiplier); | |
| // Create initial snake | |
| const startSegment = this.segmentPool.get(); | |
| startSegment.position.set(0, 0, 0); | |
| this.snake.push(startSegment); | |
| this.scene.add(startSegment); | |
| // Create food | |
| this.chooseFoodType(); | |
| this.placeFood(); | |
| // Create obstacles | |
| this.createObstacles(); | |
| // Update score display | |
| document.getElementById('info').textContent = `Score: ${this.score} | High: ${this.highScore}`; | |
| // Start background music | |
| const music = document.getElementById('bgMusic'); | |
| music.volume = 0.3; | |
| music.play(); | |
| } | |
| // Start the game | |
| startGame() { | |
| this.resetGame(); | |
| GameState.changeState(GameState.PLAYING); | |
| this.gameLoop(); | |
| } | |
| // Game over | |
| triggerGameOver() { | |
| this.isGameOver = true; | |
| // Update final score display | |
| document.getElementById('finalScore').textContent = `Final Score: ${this.score}`; | |
| // Check for high score | |
| const highScores = this.loadHighScores(); | |
| if (this.score > 0) { | |
| // Add current score to high scores | |
| highScores.push({ | |
| score: this.score, | |
| difficulty: this.currentDifficulty, | |
| date: new Date().toLocaleDateString() | |
| }); | |
| // Sort high scores | |
| highScores.sort((a, b) => b.score - a.score); | |
| // Keep only top scores | |
| const topScores = highScores.slice(0, CONFIG.HIGH_SCORES_COUNT); | |
| // Save high scores | |
| localStorage.setItem('snakeHighScores', JSON.stringify(topScores)); | |
| // Update high score if needed | |
| this.highScore = Math.max(this.score, this.highScore); | |
| } | |
| // Update high scores table | |
| this.updateHighScoresTable(); | |
| // Stop background music | |
| document.getElementById('bgMusic').pause(); | |
| // Change game state to game over | |
| GameState.changeState(GameState.GAME_OVER); | |
| } | |
| // Game loop | |
| gameLoop(time) { | |
| // Update current time | |
| if (!time) time = 0; | |
| // Update game | |
| this.update(time); | |
| // Render scene | |
| this.render(); | |
| // Continue game loop | |
| this.gameLoopId = requestAnimationFrame(this.gameLoop.bind(this)); | |
| } | |
| // Render scene | |
| render() { | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| // Initialize game | |
| init() { | |
| // Create scene | |
| this.scene = new THREE.Scene(); | |
| this.scene.background = new THREE.Color(0x000000); | |
| // Add fog for depth | |
| this.scene.fog = new THREE.Fog(0x000500, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 2.5); | |
| // Create camera | |
| this.camera = new THREE.PerspectiveCamera( | |
| 65, window.innerWidth / window.innerHeight, 0.1, 1000 | |
| ); | |
| this.camera.position.set(0, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 0.9); | |
| this.camera.lookAt(0, -CONFIG.GRID_SIZE * 0.1, 0); | |
| // Create renderer | |
| this.renderer = new THREE.WebGLRenderer({ | |
| canvas: document.getElementById('gameCanvas'), | |
| antialias: true, | |
| alpha: true | |
| }); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| this.renderer.setPixelRatio(window.devicePixelRatio); | |
| // Create grid for visual reference | |
| const gridHelper = new THREE.GridHelper( | |
| CONFIG.GRID_SIZE * CONFIG.CELL_SIZE, | |
| CONFIG.GRID_SIZE, | |
| 0x005500, | |
| 0x003300 | |
| ); | |
| gridHelper.position.y = -CONFIG.CELL_SIZE / 2; | |
| this.scene.add(gridHelper); | |
| // Add ambient light | |
| const ambientLight = new THREE.AmbientLight(0x404060); | |
| this.scene.add(ambientLight); | |
| // Add directional light | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
| directionalLight.position.set(5, 10, 7); | |
| this.scene.add(directionalLight); | |
| // Create head light | |
| this.headLight = new THREE.PointLight(0x00ff00, 1, CONFIG.CELL_SIZE * 3); | |
| this.scene.add(this.headLight); | |
| // Create particle system | |
| this.particleSystem = new ParticleSystem(this.scene); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| } | |
| // Set up event listeners | |
| setupEventListeners() { | |
| // Keyboard controls | |
| document.addEventListener('keydown', this.handleKeyDown.bind(this)); | |
| // Touch controls with prevention | |
| const touchControls = document.getElementById('touchControls'); | |
| const preventDefault = (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }; | |
| document.getElementById('upBtn').addEventListener('touchstart', (e) => { | |
| preventDefault(e); | |
| this.handleDirectionChange(0, 0, -1); | |
| }); | |
| document.getElementById('downBtn').addEventListener('touchstart', (e) => { | |
| preventDefault(e); | |
| this.handleDirectionChange(0, 0, 1); | |
| }); | |
| document.getElementById('leftBtn').addEventListener('touchstart', (e) => { | |
| preventDefault(e); | |
| this.handleDirectionChange(-1, 0, 0); | |
| }); | |
| document.getElementById('rightBtn').addEventListener('touchstart', (e) => { | |
| preventDefault(e); | |
| this.handleDirectionChange(1, 0, 0); | |
| }); | |
| // Prevent touch events on game canvas | |
| document.getElementById('gameCanvas').addEventListener('touchstart', preventDefault, { passive: false }); | |
| document.getElementById('gameCanvas').addEventListener('touchmove', preventDefault, { passive: false }); | |
| // Show touch controls on mobile devices | |
| if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { | |
| touchControls.style.display = 'block'; | |
| } | |
| // UI buttons | |
| document.getElementById('startBtn').addEventListener('click', () => { | |
| this.startGame(); | |
| }); | |
| document.getElementById('restartBtn').addEventListener('click', () => { | |
| this.startGame(); | |
| }); | |
| document.getElementById('menuBtn').addEventListener('click', () => { | |
| GameState.changeState(GameState.MENU); | |
| }); | |
| document.getElementById('difficultyBtn').addEventListener('click', () => { | |
| this.cycleDifficulty(); | |
| }); | |
| // Pause screen buttons | |
| document.getElementById('resumeBtn').addEventListener('click', () => { | |
| this.togglePause(); | |
| }); | |
| document.getElementById('quitBtn').addEventListener('click', () => { | |
| GameState.changeState(GameState.MENU); | |
| }); | |
| } | |
| // Cycle through difficulty levels | |
| cycleDifficulty() { | |
| const difficulties = Object.keys(CONFIG.DIFFICULTY_LEVELS); | |
| const currentIndex = difficulties.indexOf(this.currentDifficulty); | |
| const nextIndex = (currentIndex + 1) % difficulties.length; | |
| this.currentDifficulty = difficulties[nextIndex]; | |
| document.getElementById('difficultyBtn').textContent = `DIFFICULTY: ${this.currentDifficulty}`; | |
| } | |
| // Handle keyboard input | |
| handleKeyDown(event) { | |
| if (GameState.currentState === GameState.PLAYING) { | |
| switch(event.key) { | |
| case 'ArrowUp': | |
| this.handleDirectionChange(0, 0, -1); | |
| event.preventDefault(); | |
| break; | |
| case 'ArrowDown': | |
| this.handleDirectionChange(0, 0, 1); | |
| event.preventDefault(); | |
| break; | |
| case 'ArrowLeft': | |
| this.handleDirectionChange(-1, 0, 0); | |
| event.preventDefault(); | |
| break; | |
| case 'ArrowRight': | |
| this.handleDirectionChange(1, 0, 0); | |
| event.preventDefault(); | |
| break; | |
| case 'p': | |
| case 'P': | |
| this.togglePause(); | |
| event.preventDefault(); | |
| break; | |
| } | |
| } else if (GameState.currentState === GameState.GAME_OVER || | |
| GameState.currentState === GameState.MENU) { | |
| if (event.key === 'Enter') { | |
| this.startGame(); | |
| event.preventDefault(); | |
| } | |
| } | |
| } | |
| // Handle direction change | |
| handleDirectionChange(x, y, z) { | |
| const newDirection = new THREE.Vector3(x, y, z).normalize().multiplyScalar(CONFIG.CELL_SIZE); | |
| // Prevent 180-degree turns (moving directly backwards) | |
| if (this.direction.dot(newDirection) === -CONFIG.CELL_SIZE * CONFIG.CELL_SIZE) { | |
| return; | |
| } | |
| this.nextDirection = newDirection; | |
| } | |
| // Toggle pause state | |
| togglePause() { | |
| this.isPaused = !this.isPaused; | |
| if (this.isPaused) { | |
| // TODO: Show pause screen | |
| document.getElementById('bgMusic').pause(); | |
| } else { | |
| document.getElementById('bgMusic').play(); | |
| } | |
| } | |
| // Load high scores from local storage | |
| loadHighScores() { | |
| const scores = localStorage.getItem('snakeHighScores'); | |
| return scores ? JSON.parse(scores) : []; | |
| } | |
| // Update high scores table | |
| updateHighScoresTable() { | |
| const highScores = this.loadHighScores(); | |
| const table = document.getElementById('scoresTable'); | |
| // Clear table except header | |
| while (table.rows.length > 1) { | |
| table.deleteRow(1); | |
| } | |
| // Add high scores to table | |
| for (let i = 0; i < highScores.length; i++) { | |
| const row = table.insertRow(-1); | |
| const rankCell = row.insertCell(0); | |
| rankCell.textContent = i + 1; | |
| const scoreCell = row.insertCell(1); | |
| scoreCell.textContent = highScores[i].score; | |
| const difficultyCell = row.insertCell(2); | |
| difficultyCell.textContent = highScores[i].difficulty; | |
| } | |
| } | |
| } | |
| // Create and start the game | |
| const game = new SnakeGame(); | |
| // Start the game when the page loads | |
| window.addEventListener('load', () => { | |
| GameState.changeState(GameState.MENU); | |
| }); | |
| </script> | |
| </body> | |
| </html> |