|
|
<!DOCTYPE html> |
|
|
<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 { |
|
|
font-size: 1.2em; |
|
|
margin-right: 5px; |
|
|
} |
|
|
|
|
|
|
|
|
.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; |
|
|
} |
|
|
|
|
|
|
|
|
.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', () => { |
|
|
|
|
|
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; |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
const canvas = document.getElementById('simulationCanvas'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
|
|
|
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; |
|
|
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); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const steering = outputs[1] - outputs[0]; |
|
|
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) { |
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const progress = this.checkpointIndex / track.checkpoints.length; |
|
|
if (progress > bestCarProgress) { |
|
|
bestCarProgress = progress; |
|
|
bestProgressBar.style.width = `${progress * 100}%`; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
if (this.isBest) { |
|
|
|
|
|
ctx.fillStyle = 'rgba(220, 38, 38, 0.9)'; |
|
|
|
|
|
|
|
|
ctx.fillRect(-6, -10, 12, 20); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#000'; |
|
|
ctx.fillRect(-7, -8, 2, 4); |
|
|
ctx.fillRect(5, -8, 2, 4); |
|
|
ctx.fillRect(-7, 4, 2, 4); |
|
|
ctx.fillRect(5, 4, 2, 4); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#60a5fa'; |
|
|
ctx.fillRect(-4, -8, 8, 6); |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
ctx.fillStyle = this.color; |
|
|
|
|
|
|
|
|
ctx.fillRect(-6, -10, 12, 20); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#000'; |
|
|
ctx.fillRect(-7, -7, 2, 3); |
|
|
ctx.fillRect(5, -7, 2, 3); |
|
|
ctx.fillRect(-7, 4, 2, 3); |
|
|
ctx.fillRect(5, 4, 2, 3); |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
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]; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
mutationValue.textContent = `${Math.round(mutationRate * 100)}%`; |
|
|
mutationSlider.value = Math.round(mutationRate * 100); |
|
|
|
|
|
|
|
|
const eliteCount = Math.max(1, Math.floor(populationSize * 0.05)); |
|
|
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; |
|
|
}); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const newPopulation = []; |
|
|
|
|
|
|
|
|
const restoredBrain = NeuralNetwork.fromJSON(modelData.brain); |
|
|
const bestCar = new Car(restoredBrain); |
|
|
bestCar.isBest = true; |
|
|
newPopulation.push(bestCar); |
|
|
|
|
|
|
|
|
for (let i = 1; i < populationSize; i++) { |
|
|
const car = bestCar.clone(); |
|
|
car.brain.mutate(mutationRate); |
|
|
newPopulation.push(car); |
|
|
} |
|
|
|
|
|
|
|
|
cars = newPopulation; |
|
|
|
|
|
|
|
|
cars.forEach(car => car.reset()); |
|
|
|
|
|
|
|
|
createConfetti(50, canvas.width/2, canvas.height/2); |
|
|
|
|
|
return true; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error loading model:', error); |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
const MAX_CONFETTI = 300; |
|
|
const confetti = []; |
|
|
|
|
|
function createConfetti(count, x, y) { |
|
|
|
|
|
if (confetti.length > MAX_CONFETTI) { |
|
|
|
|
|
confetti.splice(0, count); |
|
|
} |
|
|
|
|
|
const actualCount = Math.min(count, 50); |
|
|
|
|
|
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, |
|
|
maxLife: 1 + Math.random() |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateConfetti(dt) { |
|
|
for (let i = confetti.length - 1; i >= 0; i--) { |
|
|
const particle = confetti[i]; |
|
|
|
|
|
|
|
|
particle.x += particle.vx * dt * 60; |
|
|
particle.y += particle.vy * dt * 60; |
|
|
particle.vy += particle.gravity * dt * 60; |
|
|
|
|
|
|
|
|
particle.life -= 0.016 * dt * 60; |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
if (bestCar && bestCar.checkpointIndex === track.checkpoints.length && !window.courseCompleted) { |
|
|
|
|
|
window.courseCompleted = true; |
|
|
|
|
|
|
|
|
createConfetti(50, canvas.width/2, canvas.height/2); |
|
|
|
|
|
|
|
|
setTimeout(() => createConfetti(25, canvas.width/4, canvas.height/2), 300); |
|
|
setTimeout(() => createConfetti(25, 3*canvas.width/4, canvas.height/2), 600); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
isRunning = false; |
|
|
cancelAnimationFrame(animationId); |
|
|
|
|
|
|
|
|
document.getElementById('continueBtn').addEventListener('click', () => { |
|
|
document.body.removeChild(message); |
|
|
isRunning = true; |
|
|
window.courseCompleted = false; |
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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++; |
|
|
}); |
|
|
|
|
|
|
|
|
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)'; |
|
|
|
|
|
|
|
|
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> |