Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="Content-Type" content="text/html; 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><i class="fas fa-car-side"></i> Evolution 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', () => { | |
// Add roundRect polyfill for browsers that don't support it | |
if (!CanvasRenderingContext2D.prototype.roundRect) { | |
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) { | |
if (typeof radius === 'undefined') { | |
radius = 5; | |
} | |
this.beginPath(); | |
this.moveTo(x + radius, y); | |
this.lineTo(x + width - radius, y); | |
this.arcTo(x + width, y, x + width, y + radius, radius); | |
this.lineTo(x + width, y + height - radius); | |
this.arcTo(x + width, y + height, x + width - radius, y + height, radius); | |
this.lineTo(x + radius, y + height); | |
this.arcTo(x, y + height, x, y + height - radius, radius); | |
this.lineTo(x, y + radius); | |
this.arcTo(x, y, x + radius, y, radius); | |
this.closePath(); | |
return this; | |
}; | |
} | |
// 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 - using rectangles instead of roundRect to avoid issues | |
ctx.fillRect(-6, -10, 12, 20); | |
// 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.fillRect(-4, -8, 8, 6); | |
// 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 - using rectangles | |
ctx.fillRect(-6, -10, 12, 20); | |
// 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; | |
} | |
// Reduce maximum number of confetti particles | |
const MAX_CONFETTI = 300; | |
const confetti = []; | |
function createConfetti(count, x, y) { | |
// Limit the number of particles to prevent performance issues | |
if (confetti.length > MAX_CONFETTI) { | |
// Remove older particles if we exceed the limit | |
confetti.splice(0, count); | |
} | |
const actualCount = Math.min(count, 50); // Limit particles per burst | |
for (let i = 0; i < actualCount; 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 checkCourseCompletion() { | |
const bestCar = getBestCar(); | |
// Prevent function from firing multiple times for the same completion | |
if (bestCar && bestCar.checkpointIndex === track.checkpoints.length && !window.courseCompleted) { | |
// Set a flag to prevent multiple triggers | |
window.courseCompleted = true; | |
// Launch a moderate amount of celebratory confetti | |
createConfetti(50, canvas.width/2, canvas.height/2); | |
// Use setTimeout for additional confetti bursts to spread them out | |
setTimeout(() => createConfetti(25, canvas.width/4, canvas.height/2), 300); | |
setTimeout(() => createConfetti(25, 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><i class="fas fa-trophy"></i> Course Completed! <i class="fas fa-trophy"></i></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; | |
window.courseCompleted = false; // Reset the completion flag | |
lastUpdateTime = performance.now(); | |
animate(); | |
}); | |
} | |
} | |
// ์๋ฎฌ๋ ์ด์ ์ํ | |
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(); | |
animate(); // Fix: Changed from init() to animate() | |
} | |
// ๊ฒฐ๊ณผ ์ ๋ฐ์ดํธ ์๋ ์ ํ (๋งค ํ๋ ์๋ง๋ค ํ์ง ์๊ณ 10ํ๋ ์๋ง๋ค ํ ๋ฒ์ฉ) | |
let updateFrameCount = 0; | |
// ์ฃผ์ ์ ๋๋ฉ์ด์ ๋ฃจํ | |
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)'; | |
// Check if the best car has completed the course | |
checkCourseCompletion(); | |
} | |
} | |
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ | |
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> |