Spaces:
Running
Running
| // Feature to detect when we've completed the course | |
| function checkCourseCompletion() { | |
| const bestCar = getBestCar(); | |
| if (bestCar && bestCar.checkpointIndex === track.checkpoints.length) { | |
| // Launch a bunch of celebratory confetti! | |
| createConfetti(100, canvas.width/2, canvas.height/2); | |
| setTimeout(() => createConfetti(50, canvas.width/4, canvas.height/2), 300); | |
| setTimeout(() => createConfetti(50, 3*canvas.width/4, canvas.height/2), 600); | |
| // Show victory message | |
| const message = document.createElement('div'); | |
| message.style.position = 'absolute'; | |
| message.style.top = '50%'; | |
| message.style.left = '50%'; | |
| message.style.transform = 'translate(-50%, -50%)'; | |
| message.style.background = 'rgba(16, 185, 129, 0.9)'; | |
| message.style.color = 'white'; | |
| message.style.padding = '20px'; | |
| message.style.borderRadius = '10px'; | |
| message.style.fontSize = '24px'; | |
| message.style.fontWeight = 'bold'; | |
| message.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)'; | |
| message.style.zIndex = '1000'; | |
| message.style.textAlign = 'center'; | |
| message.innerHTML = ` | |
| <div>๐ Course Completed! ๐</div> | |
| <div style="font-size: 16px; margin-top: 10px;"> | |
| Generations: ${generation}<br> | |
| Fitness: ${Math.round(bestCar.fitness * 1000)} | |
| </div> | |
| <button id="continueBtn" style=" | |
| background-color: white; | |
| color: #10b981; | |
| border: none; | |
| padding: 8px 16px; | |
| border-radius: 5px; | |
| margin-top: 15px; | |
| cursor: pointer; | |
| font-weight: bold;"> | |
| Continue Training | |
| </button> | |
| `; | |
| document.body.appendChild(message); | |
| // Pause simulation | |
| isRunning = false; | |
| cancelAnimationFrame(animationId); | |
| // Event listener for the continue button | |
| document.getElementById('continueBtn').addEventListener('click', () => { | |
| document.body.removeChild(message); | |
| isRunning = true; | |
| lastUpdateTime = performance.now(); | |
| animate(); | |
| }); | |
| } | |
| } | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Driving Simulation</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" /> | |
| <style> | |
| body { | |
| background-color: #111827; | |
| color: #f3f4f6; | |
| font-family: Arial, sans-serif; | |
| line-height: 1.5; | |
| margin: 0; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| h1 { | |
| color: #60a5fa; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); | |
| letter-spacing: 1px; | |
| } | |
| canvas { | |
| background-color: #2d3748; | |
| border-radius: 12px; | |
| display: block; | |
| margin: 0 auto 20px; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1); | |
| border: 2px solid #4a5568; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| justify-content: center; | |
| flex-wrap: wrap; | |
| } | |
| button { | |
| background-color: #3b82f6; | |
| color: white; | |
| border: none; | |
| padding: 10px 18px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| } | |
| button:hover { | |
| background-color: #2563eb; | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| } | |
| button:active { | |
| transform: translateY(1px); | |
| } | |
| .start-btn { | |
| background-color: #10b981; | |
| } | |
| .start-btn:hover { | |
| background-color: #059669; | |
| } | |
| .pause-btn { | |
| background-color: #f59e0b; | |
| } | |
| .pause-btn:hover { | |
| background-color: #d97706; | |
| } | |
| .reset-btn { | |
| background-color: #ef4444; | |
| } | |
| .reset-btn:hover { | |
| background-color: #dc2626; | |
| } | |
| .stats { | |
| display: flex; | |
| justify-content: space-between; | |
| background-color: #1f2937; | |
| padding: 18px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| border: 1px solid #374151; | |
| } | |
| .stat-item { | |
| text-align: center; | |
| background-color: #2d3748; | |
| padding: 10px 15px; | |
| border-radius: 8px; | |
| min-width: 90px; | |
| transition: all 0.3s ease; | |
| } | |
| .stat-item:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .stat-value { | |
| font-size: 1.4em; | |
| font-weight: bold; | |
| color: #60a5fa; | |
| margin-top: 5px; | |
| } | |
| .settings { | |
| background-color: #1f2937; | |
| padding: 20px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| border: 1px solid #374151; | |
| } | |
| .slider-container { | |
| margin-bottom: 15px; | |
| background-color: #2d3748; | |
| padding: 12px; | |
| border-radius: 8px; | |
| } | |
| .slider-container label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 500; | |
| color: #d1d5db; | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| margin-bottom: 8px; | |
| height: 6px; | |
| -webkit-appearance: none; | |
| background: #4b5563; | |
| border-radius: 5px; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| background: #3b82f6; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| background: #2563eb; | |
| } | |
| .progress-container { | |
| background-color: #374151; | |
| height: 10px; | |
| border-radius: 5px; | |
| margin-top: 10px; | |
| overflow: hidden; | |
| box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, #10b981, #3b82f6); | |
| width: 0; | |
| transition: width 0.3s; | |
| box-shadow: 0 0 5px rgba(16, 185, 129, 0.5); | |
| } | |
| /* Car emoji styling */ | |
| .car-emoji { | |
| font-size: 1.2em; | |
| margin-right: 5px; | |
| } | |
| /* Added visual elements */ | |
| .section-title { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| border-bottom: 1px solid #374151; | |
| padding-bottom: 8px; | |
| } | |
| .section-title i { | |
| margin-right: 8px; | |
| color: #60a5fa; | |
| } | |
| /* Custom checkbox */ | |
| .toggle-switch { | |
| position: relative; | |
| display: inline-block; | |
| width: 50px; | |
| height: 24px; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .toggle-slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #4b5563; | |
| transition: .4s; | |
| border-radius: 24px; | |
| } | |
| .toggle-slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 16px; | |
| width: 16px; | |
| left: 4px; | |
| bottom: 4px; | |
| background-color: white; | |
| transition: .4s; | |
| border-radius: 50%; | |
| } | |
| input:checked + .toggle-slider { | |
| background-color: #3b82f6; | |
| } | |
| input:checked + .toggle-slider:before { | |
| transform: translateX(26px); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>๐๏ธ AI Driving Simulation</h1> | |
| <canvas id="simulationCanvas" width="800" height="500"></canvas> | |
| <div class="controls"> | |
| <button id="startBtn" class="start-btn"><i class="fas fa-play"></i> Start</button> | |
| <button id="pauseBtn" class="pause-btn"><i class="fas fa-pause"></i> Pause</button> | |
| <button id="resetBtn" class="reset-btn"><i class="fas fa-sync-alt"></i> New Track</button> | |
| <button id="saveBtn"><i class="fas fa-save"></i> Save Model</button> | |
| <button id="loadBtn"><i class="fas fa-upload"></i> Load Model</button> | |
| </div> | |
| <div class="stats"> | |
| <div class="stat-item"> | |
| <div><i class="fas fa-dna"></i> Generation</div> | |
| <div id="generationCount" class="stat-value">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div><i class="fas fa-car"></i> Alive</div> | |
| <div class="stat-value"><span id="aliveCount">0</span>/<span id="populationCount">0</span></div> | |
| </div> | |
| <div class="stat-item"> | |
| <div><i class="fas fa-trophy"></i> Best Fitness</div> | |
| <div id="maxFitness" class="stat-value">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div><i class="fas fa-tachometer-alt"></i> FPS</div> | |
| <div id="fpsCounter" class="stat-value">0</div> | |
| </div> | |
| </div> | |
| <div class="settings"> | |
| <div class="section-title"> | |
| <i class="fas fa-sliders-h"></i> <h3>Simulation Settings</h3> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="populationSlider"><i class="fas fa-users"></i> Population Size:</label> | |
| <input type="range" id="populationSlider" min="10" max="300" value="100"> | |
| <span id="populationValue">100</span> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="mutationSlider"><i class="fas fa-random"></i> Mutation Rate:</label> | |
| <input type="range" id="mutationSlider" min="1" max="100" value="10"> | |
| <span id="mutationValue">10%</span> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="speedSlider"><i class="fas fa-fast-forward"></i> Simulation Speed:</label> | |
| <input type="range" id="speedSlider" min="1" max="30" value="5"> | |
| <span id="speedValue">15x</span> | |
| </div> | |
| <div class="slider-container"> | |
| <label><i class="fas fa-flag-checkered"></i> Track Progress:</label> | |
| <div class="progress-container"> | |
| <div id="bestProgressBar" class="progress-bar"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="settings"> | |
| <div class="section-title"> | |
| <i class="fas fa-info-circle"></i> <h3>About This Simulation</h3> | |
| </div> | |
| <p>This simulation demonstrates how AI can learn to drive using genetic algorithms and neural networks. Cars must navigate randomly generated tracks without any prior knowledge of the environment.</p> | |
| <p><strong>Key Improvements:</strong></p> | |
| <ul> | |
| <li><i class="fas fa-brain"></i> <strong>Enhanced Neural Network:</strong> Using Sigmoid activation function for smoother decision making</li> | |
| <li><i class="fas fa-random"></i> <strong>Crossover:</strong> Combining the best traits from parent models</li> | |
| <li><i class="fas fa-chart-line"></i> <strong>Adaptive Mutation:</strong> Automatically adjusts as generations progress</li> | |
| <li><i class="fas fa-bolt"></i> <strong>Performance Optimization:</strong> Delta-time based updates for consistent simulation</li> | |
| <li><i class="fas fa-exclamation-triangle"></i> <strong>Improved Collision Detection:</strong> More accurate polygon-based detection</li> | |
| <li><i class="fas fa-save"></i> <strong>Model Saving:</strong> Save and load your best models</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Canvas ์ค์ | |
| const canvas = document.getElementById('simulationCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // UI ์์ | |
| const startBtn = document.getElementById('startBtn'); | |
| const pauseBtn = document.getElementById('pauseBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const saveBtn = document.getElementById('saveBtn'); | |
| const loadBtn = document.getElementById('loadBtn'); | |
| const populationSlider = document.getElementById('populationSlider'); | |
| const mutationSlider = document.getElementById('mutationSlider'); | |
| const speedSlider = document.getElementById('speedSlider'); | |
| const populationValue = document.getElementById('populationValue'); | |
| const mutationValue = document.getElementById('mutationValue'); | |
| const speedValue = document.getElementById('speedValue'); | |
| const generationCount = document.getElementById('generationCount'); | |
| const aliveCount = document.getElementById('aliveCount'); | |
| const populationCount = document.getElementById('populationCount'); | |
| const maxFitness = document.getElementById('maxFitness'); | |
| const fpsCounter = document.getElementById('fpsCounter'); | |
| const bestProgressBar = document.getElementById('bestProgressBar'); | |
| // ์๋ฎฌ๋ ์ด์ ๋งค๊ฐ๋ณ์ | |
| let populationSize = parseInt(populationSlider.value); | |
| let mutationRate = parseInt(mutationSlider.value) / 100; | |
| let simulationSpeed = parseInt(speedSlider.value) * 3; // 3๋ฐฐ ๋น ๋ฅธ ์๋ | |
| let isRunning = false; | |
| let generation = 0; | |
| let fps = 0; | |
| let bestCarProgress = 0; | |
| let deltaTime = 0; | |
| let lastUpdateTime = 0; | |
| let frameCount = 0; | |
| let lastFpsUpdate = 0; | |
| // ํ์ฑํ ํจ์ | |
| const sigmoid = (x) => 1 / (1 + Math.exp(-x)); | |
| const relu = (x) => Math.max(0, x); | |
| // ํธ๋ ์ ์ | |
| const track = { | |
| walls: [], | |
| checkpoints: [], | |
| startPosition: { x: 100, y: 250, angle: 0 }, | |
| generateRandomTrack() { | |
| this.walls = []; | |
| this.checkpoints = []; | |
| // ์ธ๋ถ ๊ฒฝ๊ณ ๋ฒฝ (ํญ์ ์กด์ฌ) | |
| this.walls.push( | |
| { x: 50, y: 50, width: 700, height: 20 }, // ์๋จ | |
| { x: 50, y: 50, width: 20, height: 400 }, // ์ข์ธก | |
| { x: 50, y: 430, width: 700, height: 20 }, // ํ๋จ | |
| { x: 730, y: 50, width: 20, height: 400 } // ์ฐ์ธก | |
| ); | |
| // ๋ฌด์์ ์ฅ์ ๋ฌผ ์์ฑ | |
| const obstacleCount = 3 + Math.floor(Math.random() * 6); | |
| for (let i = 0; i < obstacleCount; i++) { | |
| const isVertical = Math.random() > 0.5; | |
| let x, y, width, height; | |
| if (isVertical) { | |
| width = 20; | |
| height = 50 + Math.random() * 200; | |
| x = 100 + Math.random() * 600; | |
| y = 100 + Math.random() * (400 - height); | |
| } else { | |
| width = 50 + Math.random() * 200; | |
| height = 20; | |
| x = 100 + Math.random() * (700 - width); | |
| y = 100 + Math.random() * 300; | |
| } | |
| // ์์ ์์น๋ฅผ ๋ง์ง ์๋๋ก ํ์ธ | |
| if (!(x < 150 && y < 300 && y + height > 200)) { | |
| this.walls.push({ x, y, width, height }); | |
| } | |
| } | |
| // ์ฒดํฌํฌ์ธํธ ์์ฑ | |
| const checkpointCount = 3 + Math.floor(Math.random() * 3); | |
| const checkpointSize = 30; | |
| // ์ฅ์ ๋ฌผ ์ฃผ๋ณ์ ํต๊ณผํด์ผ ํ๋ ์์น ์์ฑ | |
| const possiblePositions = [ | |
| { x: 700, y: 100 }, // ์ฐ์ธก ์๋จ | |
| { x: 600, y: 400 }, // ์ฐ์ธก ํ๋จ ์ค์ | |
| { x: 300, y: 400 }, // ํ๋จ ์ค์ | |
| { x: 100, y: 300 }, // ์ข์ธก ์ค์ | |
| { x: 400, y: 100 }, // ์๋จ ์ค์ | |
| { x: 200, y: 200 }, // ์ข์ธก ์ค์ | |
| { x: 600, y: 200 } // ์ฐ์ธก ์ค์ | |
| ]; | |
| // ์ ํํ๊ณ ์ฒดํฌํฌ์ธํธ ์๋งํผ ์ ํ | |
| const shuffled = [...possiblePositions].sort(() => 0.5 - Math.random()); | |
| for (let i = 0; i < checkpointCount; i++) { | |
| const pos = shuffled[i]; | |
| this.checkpoints.push({ | |
| x: pos.x, | |
| y: pos.y, | |
| width: checkpointSize, | |
| height: checkpointSize | |
| }); | |
| } | |
| // ์์ ์์น ์ค์ (ํญ์ ์ข์ธก, ์์ง ์์น๋ ๋ฌด์์) | |
| this.startPosition = { | |
| x: 100, | |
| y: 100 + Math.random() * 300, | |
| angle: 0 | |
| }; | |
| }, | |
| draw(ctx) { | |
| // ๋ฒฝ ๊ทธ๋ฆฌ๊ธฐ | |
| ctx.fillStyle = '#4a5568'; | |
| this.walls.forEach(wall => { | |
| ctx.fillRect(wall.x, wall.y, wall.width, wall.height); | |
| }); | |
| // ์ฒดํฌํฌ์ธํธ ๊ทธ๋ฆฌ๊ธฐ | |
| ctx.fillStyle = 'rgba(74, 222, 128, 0.3)'; | |
| this.checkpoints.forEach((checkpoint, index) => { | |
| ctx.fillRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height); | |
| // Add checkpoint number | |
| ctx.fillStyle = 'white'; | |
| ctx.font = '12px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText((index + 1).toString(), | |
| checkpoint.x + checkpoint.width/2, | |
| checkpoint.y + checkpoint.height/2); | |
| ctx.fillStyle = 'rgba(74, 222, 128, 0.3)'; | |
| }); | |
| // ์์ ์์น ๊ทธ๋ฆฌ๊ธฐ | |
| ctx.fillStyle = 'rgba(96, 165, 250, 0.5)'; | |
| ctx.fillRect(this.startPosition.x - 15, this.startPosition.y - 25, 30, 50); | |
| // Draw start flag | |
| ctx.fillStyle = 'white'; | |
| ctx.font = '14px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText("START", this.startPosition.x, this.startPosition.y + 15); | |
| } | |
| }; | |
| // ์๋์ฐจ ํด๋์ค | |
| class Car { | |
| constructor(brain) { | |
| this.reset(); | |
| this.brain = brain ? brain : new NeuralNetwork([5, 8, 2]); | |
| this.fitness = 0; | |
| this.checkpointIndex = 0; | |
| this.sensors = [0, 0, 0, 0, 0]; // ์ ๋ฐฉ, ์ข์ธก, ์ฐ์ธก, ์ข์ ๋ฐฉ, ์ฐ์ ๋ฐฉ | |
| this.sensorAngles = [0, -Math.PI/4, Math.PI/4, -Math.PI/8, Math.PI/8]; | |
| this.sensorLength = 100; | |
| this.color = 'rgba(59, 130, 246, 0.8)'; | |
| this.isBest = false; | |
| this.lastPosition = { x: 0, y: 0 }; | |
| this.stuckTime = 0; // ์๋์ฐจ๊ฐ ์์ง์ด์ง ์๋ ์๊ฐ ์ถ์ | |
| } | |
| reset() { | |
| this.x = track.startPosition.x; | |
| this.y = track.startPosition.y; | |
| this.angle = track.startPosition.angle; | |
| this.speed = 0; | |
| this.maxSpeed = 10; // ์ต๋ ์๋ ์ฆ๊ฐ | |
| this.acceleration = 0.2; // ๊ฐ์๋ ์ฆ๊ฐ | |
| this.rotationSpeed = 0.1; // ํ์ ์๋ ์ฆ๊ฐ | |
| this.damaged = false; | |
| this.checkpointIndex = 0; | |
| this.fitness = 0; | |
| this.stuckTime = 0; | |
| this.lastPosition = { x: this.x, y: this.y }; | |
| } | |
| update(dt) { | |
| if (this.damaged) return; | |
| // ์ด์ ์์น ์ ์ฅ | |
| this.lastPosition = { x: this.x, y: this.y }; | |
| // ์ผ์ ์ ๋ฐ์ดํธ | |
| this.updateSensors(); | |
| // ์ ๊ฒฝ๋ง ์ถ๋ ฅ ๊ฐ์ ธ์ค๊ธฐ | |
| const outputs = this.brain.predict(this.sensors); | |
| // ์กฐํฅ ์ ์ฉ (outputs[0] = ์ผ์ชฝ, outputs[1] = ์ค๋ฅธ์ชฝ) | |
| const steering = outputs[1] - outputs[0]; // -1์์ 1 ์ฌ์ด | |
| this.angle += steering * this.rotationSpeed * dt; | |
| // ์๋์ ์์น ์ ๋ฐ์ดํธ | |
| this.speed = this.maxSpeed; | |
| this.x += Math.sin(this.angle) * this.speed * dt; | |
| this.y -= Math.cos(this.angle) * this.speed * dt; | |
| // ์ถฉ๋ ๊ฒ์ฌ | |
| this.checkCollisions(); | |
| // ์ฒดํฌํฌ์ธํธ ๊ฒ์ฌ | |
| this.checkCheckpoints(); | |
| // ์ ์ง ํ์ธ (์๋์ฐจ๊ฐ ์์ง์ด์ง ์๋ ๊ฒฝ์ฐ) | |
| const distance = Math.sqrt( | |
| Math.pow(this.x - this.lastPosition.x, 2) + | |
| Math.pow(this.y - this.lastPosition.y, 2) | |
| ); | |
| if (distance < 0.5 * dt) { | |
| this.stuckTime += dt; | |
| if (this.stuckTime > 1.5) { // ์ ์ง ํ๋จ ์๊ฐ ๋จ์ถ (3์ด โ 1.5์ด) | |
| this.damaged = true; | |
| } | |
| } else { | |
| this.stuckTime = 0; | |
| // ์ ํฉ๋ ์ ๋ฐ์ดํธ | |
| this.fitness += distance; // ์ด๋ ๊ฑฐ๋ฆฌ์ ๋ฐ๋ฅธ ์ ํฉ๋ | |
| } | |
| } | |
| updateSensors() { | |
| this.sensors = this.sensorAngles.map(angle => { | |
| const sensorAngle = this.angle + angle; | |
| let sensorEndX = this.x + Math.sin(sensorAngle) * this.sensorLength; | |
| let sensorEndY = this.y - Math.cos(sensorAngle) * this.sensorLength; | |
| let minDistance = this.sensorLength; | |
| // ๋ชจ๋ ๋ฒฝ์ ๋ํด ๊ฒ์ฌ | |
| for (const wall of track.walls) { | |
| const intersection = this.lineRectIntersection( | |
| this.x, this.y, sensorEndX, sensorEndY, | |
| wall.x, wall.y, wall.width, wall.height | |
| ); | |
| if (intersection) { | |
| const distance = Math.sqrt( | |
| Math.pow(intersection.x - this.x, 2) + | |
| Math.pow(intersection.y - this.y, 2) | |
| ); | |
| minDistance = Math.min(minDistance, distance); | |
| } | |
| } | |
| // ์ ๊ทํ๋ ๊ฑฐ๋ฆฌ ๋ฐํ (0-1 ๋ฒ์; 1 = ์ฅ์ ๋ฌผ ์์, 0 = ์๋์ฐจ ๋ฐ๋ก ์์ ์ฅ์ ๋ฌผ) | |
| return 1 - (minDistance / this.sensorLength); | |
| }); | |
| } | |
| lineRectIntersection(x1, y1, x2, y2, rx, ry, rw, rh) { | |
| // ์ง์ ์ด ์ง์ฌ๊ฐํ์ ๋ณ๊ณผ ๊ต์ฐจํ๋์ง ํ์ธ | |
| const left = this.lineLineIntersection(x1, y1, x2, y2, rx, ry, rx, ry + rh); | |
| const right = this.lineLineIntersection(x1, y1, x2, y2, rx + rw, ry, rx + rw, ry + rh); | |
| const top = this.lineLineIntersection(x1, y1, x2, y2, rx, ry, rx + rw, ry); | |
| const bottom = this.lineLineIntersection(x1, y1, x2, y2, rx, ry + rh, rx + rw, ry + rh); | |
| let closestIntersection = null; | |
| let minDistance = Infinity; | |
| [left, right, top, bottom].forEach(intersection => { | |
| if (intersection) { | |
| const distance = Math.sqrt(Math.pow(intersection.x - x1, 2) + Math.pow(intersection.y - y1, 2)); | |
| if (distance < minDistance) { | |
| minDistance = distance; | |
| closestIntersection = intersection; | |
| } | |
| } | |
| }); | |
| return closestIntersection; | |
| } | |
| lineLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) { | |
| // ๋ ์ง์ ์ฌ์ด์ ๊ต์ฐจ์ ๊ณ์ฐ | |
| const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); | |
| if (denominator === 0) return null; // ์ง์ ์ด ํํ | |
| const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator; | |
| const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator; | |
| if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { | |
| return { | |
| x: x1 + ua * (x2 - x1), | |
| y: y1 + ua * (y2 - y1) | |
| }; | |
| } | |
| return null; | |
| } | |
| checkCollisions() { | |
| // ๋ฒฝ๊ณผ์ ์ถฉ๋ ๊ฐ์ง (๊ฐ์ ๋ ๊ณ์ฐ) | |
| // ์๋์ฐจ๋ฅผ ๋ค๊ฐํ์ผ๋ก ํํํ์ฌ ๋ ์ ํํ ์ถฉ๋ ๊ฐ์ง | |
| const carCorners = this.getCarCorners(); | |
| // ๋ชจ๋ ๋ฒฝ์ ๋ํด ๊ฒ์ฌ | |
| for (const wall of track.walls) { | |
| // ๊ฐ๋จํ ์ฌ๊ฐํ ์ถฉ๋ ํ์ธ (์ต์ ํ๋ฅผ ์ํ ์ฒซ ๋จ๊ณ) | |
| if (this.x > wall.x - 10 && this.x < wall.x + wall.width + 10 && | |
| this.y > wall.y - 10 && this.y < wall.y + wall.height + 10) { | |
| // ์๋์ฐจ ๊ผญ์ง์ ์ด ๋ฒฝ ๋ด๋ถ์ ์๋์ง ํ์ธ | |
| for (const corner of carCorners) { | |
| if (corner.x > wall.x && corner.x < wall.x + wall.width && | |
| corner.y > wall.y && corner.y < wall.y + wall.height) { | |
| this.damaged = true; | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| // ๊ฒฝ๊ณ ํ์ธ | |
| if (this.x < 0 || this.x > canvas.width || this.y < 0 || this.y > canvas.height) { | |
| this.damaged = true; | |
| } | |
| } | |
| getCarCorners() { | |
| // ์๋์ฐจ์ ๋ค ๊ผญ์ง์ ๊ณ์ฐ | |
| const width = 12; | |
| const height = 20; | |
| const cornerOffsets = [ | |
| { x: -width/2, y: -height/2 }, // ์ข์๋จ | |
| { x: width/2, y: -height/2 }, // ์ฐ์๋จ | |
| { x: width/2, y: height/2 }, // ์ฐํ๋จ | |
| { x: -width/2, y: height/2 } // ์ขํ๋จ | |
| ]; | |
| return cornerOffsets.map(offset => { | |
| const rotatedX = offset.x * Math.cos(this.angle) - offset.y * Math.sin(this.angle); | |
| const rotatedY = offset.x * Math.sin(this.angle) + offset.y * Math.cos(this.angle); | |
| return { | |
| x: this.x + rotatedX, | |
| y: this.y + rotatedY | |
| }; | |
| }); | |
| } | |
| checkCheckpoints() { | |
| if (this.checkpointIndex >= track.checkpoints.length) return; | |
| const checkpoint = track.checkpoints[this.checkpointIndex]; | |
| if (this.x > checkpoint.x && this.x < checkpoint.x + checkpoint.width && | |
| this.y > checkpoint.y && this.y < checkpoint.y + checkpoint.height) { | |
| this.checkpointIndex++; | |
| this.fitness += 1000; // Bonus for reaching checkpoint | |
| // Update best progress visualization | |
| const progress = this.checkpointIndex / track.checkpoints.length; | |
| if (progress > bestCarProgress) { | |
| bestCarProgress = progress; | |
| bestProgressBar.style.width = `${progress * 100}%`; | |
| // Add confetti effect for completed checkpoints | |
| if (this.checkpointIndex > 0) { | |
| createConfetti(10, this.x, this.y); | |
| } | |
| } | |
| } | |
| } | |
| draw(ctx) { | |
| if (this.damaged) return; | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.angle); | |
| // Draw car body - Enhanced car shape with emoji style | |
| if (this.isBest) { | |
| // Draw fancy car for the best performer | |
| ctx.fillStyle = 'rgba(220, 38, 38, 0.9)'; | |
| // Main body | |
| ctx.beginPath(); | |
| ctx.roundRect(-6, -10, 12, 20, 2); | |
| ctx.fill(); | |
| // Wheels | |
| ctx.fillStyle = '#000'; | |
| ctx.fillRect(-7, -8, 2, 4); // left front | |
| ctx.fillRect(5, -8, 2, 4); // right front | |
| ctx.fillRect(-7, 4, 2, 4); // left rear | |
| ctx.fillRect(5, 4, 2, 4); // right rear | |
| // Windshield | |
| ctx.fillStyle = '#60a5fa'; | |
| ctx.beginPath(); | |
| ctx.roundRect(-4, -8, 8, 6, 1); | |
| ctx.fill(); | |
| // Draw a small crown on top | |
| ctx.fillStyle = '#facc15'; | |
| ctx.beginPath(); | |
| ctx.moveTo(-3, -11); | |
| ctx.lineTo(-1, -13); | |
| ctx.lineTo(1, -11); | |
| ctx.lineTo(3, -13); | |
| ctx.lineTo(3, -10); | |
| ctx.lineTo(-3, -10); | |
| ctx.fill(); | |
| } else { | |
| // Regular car | |
| ctx.fillStyle = this.color; | |
| // Main body | |
| ctx.beginPath(); | |
| ctx.roundRect(-6, -10, 12, 20, 2); | |
| ctx.fill(); | |
| // Wheels (simple) | |
| ctx.fillStyle = '#000'; | |
| ctx.fillRect(-7, -7, 2, 3); // left front | |
| ctx.fillRect(5, -7, 2, 3); // right front | |
| ctx.fillRect(-7, 4, 2, 3); // left rear | |
| ctx.fillRect(5, 4, 2, 3); // right rear | |
| // Simple windshield | |
| ctx.fillStyle = '#a3e0ff'; | |
| ctx.fillRect(-4, -7, 8, 5); | |
| } | |
| // ์ต๊ณ ์๋์ฐจ์ ์ผ์ ๊ทธ๋ฆฌ๊ธฐ | |
| if (this.isBest) { | |
| ctx.restore(); // ์ปจํ ์คํธ ๋ณต์ | |
| // ์ผ์ ๋ ์ด ๊ทธ๋ฆฌ๊ธฐ | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; | |
| ctx.lineWidth = 1; | |
| this.sensorAngles.forEach((angle, i) => { | |
| const sensorAngle = this.angle + angle; | |
| const sensorValue = this.sensors[i]; | |
| const sensorLength = this.sensorLength * (1 - sensorValue); | |
| const endX = this.x + Math.sin(sensorAngle) * sensorLength; | |
| const endY = this.y - Math.cos(sensorAngle) * sensorLength; | |
| ctx.beginPath(); | |
| ctx.moveTo(this.x, this.y); | |
| ctx.lineTo(endX, endY); | |
| ctx.stroke(); | |
| }); | |
| return; // ์ด๋ฏธ ctx.restore()๋ฅผ ํธ์ถํ์ผ๋ฏ๋ก ์ฌ๊ธฐ์ ์ข ๋ฃ | |
| } | |
| ctx.restore(); | |
| } | |
| clone() { | |
| return new Car(this.brain.clone()); | |
| } | |
| } | |
| // ์ ๊ฒฝ๋ง ํด๋์ค | |
| class NeuralNetwork { | |
| constructor(neuronCounts) { | |
| this.levels = []; | |
| for (let i = 0; i < neuronCounts.length - 1; i++) { | |
| this.levels.push(new Level( | |
| neuronCounts[i], neuronCounts[i + 1] | |
| )); | |
| } | |
| } | |
| predict(givenInputs) { | |
| let outputs = Level.feedForward( | |
| givenInputs, this.levels[0] | |
| ); | |
| for (let i = 1; i < this.levels.length; i++) { | |
| outputs = Level.feedForward( | |
| outputs, this.levels[i] | |
| ); | |
| } | |
| return outputs; | |
| } | |
| clone() { | |
| const clone = new NeuralNetwork([]); | |
| clone.levels = this.levels.map(level => level.clone()); | |
| return clone; | |
| } | |
| mutate(rate) { | |
| for (const level of this.levels) { | |
| for (let i = 0; i < level.biases.length; i++) { | |
| if (Math.random() < rate) { | |
| level.biases[i] = lerp( | |
| level.biases[i], | |
| Math.random() * 2 - 1, | |
| 0.5 | |
| ); | |
| } | |
| } | |
| for (let i = 0; i < level.weights.length; i++) { | |
| for (let j = 0; j < level.weights[i].length; j++) { | |
| if (Math.random() < rate) { | |
| level.weights[i][j] = lerp( | |
| level.weights[i][j], | |
| Math.random() * 2 - 1, | |
| 0.5 | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| static crossover(parentA, parentB) { | |
| // ๋ ๋ถ๋ชจ ์ ๊ฒฝ๋ง์์ ์ ์ ๊ฒฝ๋ง ์์ฑ | |
| if (parentA.levels.length !== parentB.levels.length) { | |
| console.error("๋ถ๋ชจ ์ ๊ฒฝ๋ง ๊ตฌ์กฐ๊ฐ ๋ค๋ฆ ๋๋ค!"); | |
| return parentA.clone(); | |
| } | |
| const childNetwork = new NeuralNetwork([]); | |
| childNetwork.levels = []; | |
| for (let l = 0; l < parentA.levels.length; l++) { | |
| const levelA = parentA.levels[l]; | |
| const levelB = parentB.levels[l]; | |
| if (levelA.inputs.length !== levelB.inputs.length || | |
| levelA.outputs.length !== levelB.outputs.length) { | |
| console.error("๋ถ๋ชจ ๋ ๋ฒจ ๊ตฌ์กฐ๊ฐ ๋ค๋ฆ ๋๋ค!"); | |
| return parentA.clone(); | |
| } | |
| const childLevel = new Level(levelA.inputs.length, levelA.outputs.length); | |
| // ๊ต์ฐจ์ ์ ํ (๋จ์ผ์ ๊ต์ฐจ) | |
| const biasesSwitch = Math.floor(Math.random() * levelA.biases.length); | |
| // ๋ฐ์ด์ด์ค ๊ต์ฐจ | |
| for (let i = 0; i < childLevel.biases.length; i++) { | |
| childLevel.biases[i] = i < biasesSwitch | |
| ? levelA.biases[i] | |
| : levelB.biases[i]; | |
| } | |
| // ๊ฐ์ค์น ๊ต์ฐจ | |
| for (let i = 0; i < childLevel.weights.length; i++) { | |
| const weightSwitch = Math.floor(Math.random() * levelA.weights[i].length); | |
| for (let j = 0; j < childLevel.weights[i].length; j++) { | |
| childLevel.weights[i][j] = j < weightSwitch | |
| ? levelA.weights[i][j] | |
| : levelB.weights[i][j]; | |
| } | |
| } | |
| childNetwork.levels.push(childLevel); | |
| } | |
| return childNetwork; | |
| } | |
| toJSON() { | |
| return { | |
| levels: this.levels.map(level => ({ | |
| inputs: level.inputs, | |
| outputs: level.outputs, | |
| biases: level.biases, | |
| weights: level.weights | |
| })) | |
| }; | |
| } | |
| static fromJSON(data) { | |
| const network = new NeuralNetwork([]); | |
| network.levels = data.levels.map(levelData => { | |
| const level = new Level(levelData.inputs.length, levelData.outputs.length); | |
| level.inputs = [...levelData.inputs]; | |
| level.outputs = [...levelData.outputs]; | |
| level.biases = [...levelData.biases]; | |
| level.weights = levelData.weights.map(w => [...w]); | |
| return level; | |
| }); | |
| return network; | |
| } | |
| } | |
| function lerp(a, b, t) { | |
| return a + (b - a) * t; | |
| } | |
| class Level { | |
| constructor(inputCount, outputCount) { | |
| this.inputs = new Array(inputCount); | |
| this.outputs = new Array(outputCount); | |
| this.biases = new Array(outputCount); | |
| this.weights = []; | |
| for (let i = 0; i < inputCount; i++) { | |
| this.weights[i] = new Array(outputCount); | |
| } | |
| Level.#randomize(this); | |
| } | |
| static #randomize(level) { | |
| for (let i = 0; i < level.inputs.length; i++) { | |
| for (let j = 0; j < level.outputs.length; j++) { | |
| level.weights[i][j] = Math.random() * 2 - 1; | |
| } | |
| } | |
| for (let i = 0; i < level.biases.length; i++) { | |
| level.biases[i] = Math.random() * 2 - 1; | |
| } | |
| } | |
| static feedForward(givenInputs, level) { | |
| // ์ ๋ ฅ ๊ฐ ์ค์ | |
| for (let i = 0; i < level.inputs.length; i++) { | |
| level.inputs[i] = givenInputs[i]; | |
| } | |
| // ๊ฐ ์ถ๋ ฅ ๋ด๋ฐ์ ๋ํด ๊ฐ์ค ํฉ๊ณ ๊ณ์ฐ | |
| for (let i = 0; i < level.outputs.length; i++) { | |
| let sum = 0; | |
| for (let j = 0; j < level.inputs.length; j++) { | |
| sum += level.inputs[j] * level.weights[j][i]; | |
| } | |
| // Sigmoid ํ์ฑํ ํจ์ ์ ์ฉ | |
| level.outputs[i] = sigmoid(sum - level.biases[i]); | |
| } | |
| return level.outputs; | |
| } | |
| clone() { | |
| const clone = new Level(this.inputs.length, this.outputs.length); | |
| clone.inputs = [...this.inputs]; | |
| clone.outputs = [...this.outputs]; | |
| clone.biases = [...this.biases]; | |
| clone.weights = this.weights.map(arr => [...arr]); | |
| return clone; | |
| } | |
| } | |
| // ์ ์ ์๊ณ ๋ฆฌ์ฆ ํจ์ | |
| function nextGeneration() { | |
| generation++; | |
| generationCount.textContent = generation; | |
| // ์ ํฉ๋ ๊ณ์ฐ | |
| calculateFitness(); | |
| // ์ ์ธ๊ตฌ ์์ฑ | |
| const newPopulation = []; | |
| // ์ ์ํ ๋์ฐ๋ณ์ด์จ ์ ์ฉ | |
| const progressRate = bestCarProgress; | |
| const adaptedRate = mutationRate * (1 - progressRate * 0.5); | |
| mutationRate = Math.max(0.01, adaptedRate); // ์ต์ 1% | |
| mutationValue.textContent = `${Math.round(mutationRate * 100)}%`; | |
| mutationSlider.value = Math.round(mutationRate * 100); | |
| // ์ด์ ์ธ๋์์ ์ต๊ณ ์๋์ฐจ ์ถ๊ฐ (์๋ฆฌํฐ์ฆ) | |
| const eliteCount = Math.max(1, Math.floor(populationSize * 0.05)); // 5% ์๋ฆฌํธ | |
| const eliteCars = getTopCars(eliteCount); | |
| for (const eliteCar of eliteCars) { | |
| eliteCar.isBest = eliteCar === eliteCars[0]; | |
| newPopulation.push(eliteCar.clone()); | |
| } | |
| // ๊ต์ฐจ์ ๋์ฐ๋ณ์ด๋ก ๋๋จธ์ง ์ฑ์ฐ๊ธฐ | |
| while (newPopulation.length < populationSize) { | |
| if (Math.random() < 0.7 && newPopulation.length + 1 < populationSize) { | |
| // ๊ต์ฐจ | |
| const parentA = selectParent(); | |
| const parentB = selectParent(); | |
| const child = new Car(NeuralNetwork.crossover(parentA.brain, parentB.brain)); | |
| // ์์์๊ฒ ์ฝ๊ฐ์ ๋์ฐ๋ณ์ด ์ ์ฉ | |
| child.brain.mutate(mutationRate); | |
| newPopulation.push(child); | |
| } else { | |
| // ๋์ฐ๋ณ์ด๋ง | |
| const parent = selectParent(); | |
| const child = parent.clone(); | |
| child.brain.mutate(mutationRate); | |
| newPopulation.push(child); | |
| } | |
| } | |
| // ์ด์ ์ธ๊ตฌ ๊ต์ฒด | |
| cars = newPopulation; | |
| // ์๋์ฐจ ์ด๊ธฐํ | |
| cars.forEach(car => car.reset()); | |
| // ์งํ๋ฅ ์ด๊ธฐํ | |
| bestCarProgress = 0; | |
| bestProgressBar.style.width = '0%'; | |
| } | |
| function calculateFitness() { | |
| let sum = 0; | |
| let max = 0; | |
| cars.forEach(car => { | |
| // ์ฒดํฌํฌ์ธํธ ๋ฌ์ฑ ๋ณด๋์ค | |
| car.fitness += car.checkpointIndex * 500; | |
| sum += car.fitness; | |
| if (car.fitness > max) max = car.fitness; | |
| }); | |
| // ์ ํฉ๋ ์ ๊ทํ | |
| cars.forEach(car => { | |
| car.fitness = car.fitness / sum; | |
| }); | |
| // UI ์ ๋ฐ์ดํธ | |
| maxFitness.textContent = Math.round(max); | |
| } | |
| function getTopCars(count) { | |
| return [...cars] | |
| .sort((a, b) => b.fitness - a.fitness) | |
| .slice(0, count); | |
| } | |
| function getBestCar() { | |
| let bestCar = cars[0]; | |
| let bestFitness = cars[0].fitness; | |
| for (let i = 1; i < cars.length; i++) { | |
| if (cars[i].fitness > bestFitness) { | |
| bestFitness = cars[i].fitness; | |
| bestCar = cars[i]; | |
| } | |
| } | |
| return bestCar; | |
| } | |
| function selectParent() { | |
| // ๋ฃฐ๋ ํ ์ ํ | |
| let index = 0; | |
| let r = Math.random(); | |
| while (r > 0 && index < cars.length) { | |
| r -= cars[index].fitness; | |
| index++; | |
| } | |
| index = Math.min(cars.length - 1, Math.max(0, index - 1)); | |
| return cars[index]; | |
| } | |
| // ๋ชจ๋ธ ์ ์ฅ/๋ถ๋ฌ์ค๊ธฐ ํจ์ | |
| function saveBestModel() { | |
| const bestCar = getBestCar(); | |
| if (bestCar) { | |
| try { | |
| const modelData = { | |
| brain: bestCar.brain.toJSON(), | |
| fitness: bestCar.fitness, | |
| generation: generation, | |
| timestamp: new Date().toISOString() | |
| }; | |
| localStorage.setItem('bestCarModel', JSON.stringify(modelData)); | |
| return true; | |
| } catch (error) { | |
| console.error('Error saving model:', error); | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| function loadModel() { | |
| try { | |
| const savedModel = localStorage.getItem('bestCarModel'); | |
| if (savedModel) { | |
| const modelData = JSON.parse(savedModel); | |
| // Use saved model for new population | |
| const newPopulation = []; | |
| // Create best car with restored brain | |
| const restoredBrain = NeuralNetwork.fromJSON(modelData.brain); | |
| const bestCar = new Car(restoredBrain); | |
| bestCar.isBest = true; | |
| newPopulation.push(bestCar); | |
| // Create variants from this model to fill population | |
| for (let i = 1; i < populationSize; i++) { | |
| const car = bestCar.clone(); | |
| car.brain.mutate(mutationRate); | |
| newPopulation.push(car); | |
| } | |
| // Replace population | |
| cars = newPopulation; | |
| // Reset cars | |
| cars.forEach(car => car.reset()); | |
| // Create a celebratory confetti effect | |
| createConfetti(50, canvas.width/2, canvas.height/2); | |
| return true; | |
| } | |
| } catch (error) { | |
| console.error('Error loading model:', error); | |
| } | |
| return false; | |
| } | |
| // ์๋ฎฌ๋ ์ด์ ์ํ | |
| let cars = []; | |
| let animationId; | |
| // ์๋ฎฌ๋ ์ด์ ์ด๊ธฐํ | |
| function init() { | |
| // ๋ฌด์์ ํธ๋ ์์ฑ | |
| track.generateRandomTrack(); | |
| // ์ด๊ธฐ ์ธ๊ตฌ ์์ฑ | |
| cars = []; | |
| for (let i = 0; i < populationSize; i++) { | |
| cars.push(new Car()); | |
| } | |
| // ํต๊ณ ์ด๊ธฐํ | |
| generation = 0; | |
| generationCount.textContent = generation; | |
| populationCount.textContent = populationSize; | |
| // ์๋ฎฌ๋ ์ด์ ์์ | |
| isRunning = true; | |
| lastUpdateTime = performance.now(); | |
| init(); | |
| } | |
| // ๊ฒฐ๊ณผ ์ ๋ฐ์ดํธ ์๋ ์ ํ (๋งค ํ๋ ์๋ง๋ค ํ์ง ์๊ณ 10ํ๋ ์๋ง๋ค ํ ๋ฒ์ฉ) | |
| let updateFrameCount = 0; | |
| // Confetti particle system | |
| const confetti = []; | |
| function createConfetti(count, x, y) { | |
| for (let i = 0; i < count; i++) { | |
| confetti.push({ | |
| x: x, | |
| y: y, | |
| size: 3 + Math.random() * 5, | |
| color: `hsl(${Math.random() * 360}, 100%, 70%)`, | |
| vx: -2 + Math.random() * 4, | |
| vy: -3 - Math.random() * 2, | |
| gravity: 0.1, | |
| life: 1, // 1 = full life, 0 = dead | |
| maxLife: 1 + Math.random() | |
| }); | |
| } | |
| } | |
| function updateConfetti(dt) { | |
| for (let i = confetti.length - 1; i >= 0; i--) { | |
| const particle = confetti[i]; | |
| // Update position | |
| particle.x += particle.vx * dt * 60; | |
| particle.y += particle.vy * dt * 60; | |
| particle.vy += particle.gravity * dt * 60; | |
| // Update life | |
| particle.life -= 0.016 * dt * 60; | |
| // Remove dead particles | |
| if (particle.life <= 0) { | |
| confetti.splice(i, 1); | |
| } | |
| } | |
| } | |
| function drawConfetti(ctx) { | |
| for (const particle of confetti) { | |
| ctx.fillStyle = particle.color; | |
| ctx.globalAlpha = particle.life; | |
| ctx.fillRect( | |
| particle.x - particle.size/2, | |
| particle.y - particle.size/2, | |
| particle.size, | |
| particle.size | |
| ); | |
| } | |
| ctx.globalAlpha = 1; | |
| } | |
| // ์ฃผ์ ์ ๋๋ฉ์ด์ ๋ฃจํ | |
| function animate(currentTime = 0) { | |
| if (!isRunning) return; | |
| animationId = requestAnimationFrame(animate); | |
| // ๋ธํ ํ์ ๊ณ์ฐ | |
| deltaTime = (currentTime - lastUpdateTime) / 1000; // ์ด ๋จ์ | |
| lastUpdateTime = currentTime; | |
| // ์๋ฎฌ๋ ์ด์ ์๋ ์ ์ฉ | |
| deltaTime *= simulationSpeed; | |
| // ์ต๋ ๋ธํ ํ์ ์ ํ (์๋ฎฌ๋ ์ด์ ์์ ์ฑ์ ์ํด) | |
| deltaTime = Math.min(deltaTime, 0.2); | |
| // FPS ๊ณ์ฐ (10ํ๋ ์๋ง๋ค ์ ๋ฐ์ดํธ) | |
| frameCount++; | |
| updateFrameCount++; | |
| if (currentTime - lastFpsUpdate >= 1000) { | |
| fps = Math.round((frameCount * 1000) / (currentTime - lastFpsUpdate)); | |
| if (updateFrameCount >= 10) { | |
| fpsCounter.textContent = fps; | |
| updateFrameCount = 0; | |
| } | |
| frameCount = 0; | |
| lastFpsUpdate = currentTime; | |
| } | |
| // ์บ๋ฒ์ค ์ง์ฐ๊ธฐ | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // ํธ๋ ๊ทธ๋ฆฌ๊ธฐ | |
| track.draw(ctx); | |
| // ์๋์ฐจ ์ ๋ฐ์ดํธ ๋ฐ ๊ทธ๋ฆฌ๊ธฐ | |
| let alive = 0; | |
| cars.forEach(car => { | |
| car.update(deltaTime); | |
| car.draw(ctx); | |
| if (!car.damaged) alive++; | |
| }); | |
| // Draw confetti particles | |
| updateConfetti(deltaTime); | |
| drawConfetti(ctx); | |
| aliveCount.textContent = alive; | |
| // ๋ชจ๋ ์๋์ฐจ๊ฐ ์์๋์๋์ง ํ์ธ | |
| if (alive === 0) { | |
| nextGeneration(); | |
| } | |
| // ์ต๊ณ ์๋์ฐจ ๊ฐ์กฐ ํ์ | |
| const bestCar = getBestCar(); | |
| if (bestCar) { | |
| bestCar.isBest = true; | |
| bestCar.color = 'rgba(220, 38, 38, 0.9)'; | |
| } | |
| } | |
| // ์ด๋ฒคํธ ๋ฆฌ์ค๋ | |
| startBtn.addEventListener('click', () => { | |
| if (!isRunning) { | |
| isRunning = true; | |
| lastUpdateTime = performance.now(); | |
| animate(); | |
| } | |
| }); | |
| pauseBtn.addEventListener('click', () => { | |
| isRunning = false; | |
| cancelAnimationFrame(animationId); | |
| }); | |
| resetBtn.addEventListener('click', () => { | |
| isRunning = false; | |
| cancelAnimationFrame(animationId); | |
| init(); | |
| }); | |
| saveBtn.addEventListener('click', () => { | |
| if (saveBestModel()) { | |
| alert('Model saved successfully!'); | |
| } else { | |
| alert('Error saving model'); | |
| } | |
| }); | |
| loadBtn.addEventListener('click', () => { | |
| if (loadModel()) { | |
| alert('Model loaded successfully!'); | |
| } else { | |
| alert('No saved model found or error loading model'); | |
| } | |
| }); | |
| // ์ฌ๋ผ์ด๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋ | |
| populationSlider.addEventListener('input', () => { | |
| populationSize = parseInt(populationSlider.value); | |
| populationValue.textContent = populationSize; | |
| populationCount.textContent = populationSize; | |
| }); | |
| mutationSlider.addEventListener('input', () => { | |
| mutationRate = parseInt(mutationSlider.value) / 100; | |
| mutationValue.textContent = `${parseInt(mutationSlider.value)}%`; | |
| }); | |
| speedSlider.addEventListener('input', () => { | |
| simulationSpeed = parseInt(speedSlider.value) * 3; | |
| speedValue.textContent = `${simulationSpeed}x`; | |
| }); | |
| // ์๋ฎฌ๋ ์ด์ ์ด๊ธฐํ ๋ฐ ์์ | |
| init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |