|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>AI Driving Simulation</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<style> |
|
#simulationCanvas { |
|
background-color: #2d3748; |
|
border-radius: 0.5rem; |
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
|
} |
|
|
|
.car { |
|
position: absolute; |
|
width: 12px; |
|
height: 20px; |
|
background-color: #3b82f6; |
|
border-radius: 3px; |
|
transform-origin: center; |
|
} |
|
|
|
.track-wall { |
|
position: absolute; |
|
background-color: #4a5568; |
|
} |
|
|
|
.checkpoint { |
|
position: absolute; |
|
background-color: rgba(74, 222, 128, 0.3); |
|
} |
|
|
|
.progress-bar { |
|
transition: width 0.3s ease; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-900 text-white min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
<h1 class="text-4xl font-bold text-center mb-6 text-blue-400">AI Driving Simulation</h1> |
|
|
|
<div class="flex flex-col lg:flex-row gap-8"> |
|
<div class="lg:w-3/4"> |
|
<div class="relative"> |
|
<canvas id="simulationCanvas" width="800" height="500" class="w-full"></canvas> |
|
|
|
<div id="bestCarContainer" class="absolute top-4 left-4 bg-gray-800 bg-opacity-80 p-3 rounded-lg"> |
|
<div class="flex items-center gap-2"> |
|
<div class="w-4 h-4 bg-blue-500 rounded-sm"></div> |
|
<span class="text-sm">Best Car</span> |
|
</div> |
|
<div class="mt-2 text-xs"> |
|
<div>Generation: <span id="generationCount">0</span></div> |
|
<div>Alive: <span id="aliveCount">0</span>/<span id="populationCount">0</span></div> |
|
<div>Max Fitness: <span id="maxFitness">0</span></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4"> |
|
<div class="bg-gray-800 p-4 rounded-lg"> |
|
<h3 class="font-semibold text-blue-300 mb-2">Simulation Controls</h3> |
|
<div class="flex flex-wrap gap-2"> |
|
<button id="startBtn" class="bg-green-600 hover:bg-green-700 px-3 py-1 rounded text-sm"> |
|
Start |
|
</button> |
|
<button id="pauseBtn" class="bg-yellow-600 hover:bg-yellow-700 px-3 py-1 rounded text-sm"> |
|
Pause |
|
</button> |
|
<button id="resetBtn" class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-sm"> |
|
New Track |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-gray-800 p-4 rounded-lg"> |
|
<h3 class="font-semibold text-blue-300 mb-2">AI Settings</h3> |
|
<div class="space-y-2"> |
|
<div> |
|
<label class="text-xs block">Population:</label> |
|
<input type="range" id="populationSlider" min="10" max="500" value="100" class="w-full"> |
|
<span id="populationValue" class="text-xs">100</span> |
|
</div> |
|
<div> |
|
<label class="text-xs block">Mutation Rate:</label> |
|
<input type="range" id="mutationSlider" min="1" max="100" value="10" class="w-full"> |
|
<span id="mutationValue" class="text-xs">10%</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-gray-800 p-4 rounded-lg"> |
|
<h3 class="font-semibold text-blue-300 mb-2">Performance</h3> |
|
<div class="text-xs space-y-1"> |
|
<div>FPS: <span id="fpsCounter">0</span></div> |
|
<div>Generation Time: <span id="genTime">0</span>ms</div> |
|
<div>Best Progress: |
|
<div class="w-full bg-gray-700 h-2 rounded-full mt-1"> |
|
<div id="bestProgressBar" class="h-full bg-green-500 rounded-full progress-bar" style="width: 0%"></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="lg:w-1/4"> |
|
<div class="bg-gray-800 p-4 rounded-lg sticky top-4"> |
|
<h3 class="font-semibold text-blue-300 mb-3">How It Works</h3> |
|
<div class="text-sm space-y-3 text-gray-300"> |
|
<p>This simulation demonstrates how AI can learn to drive through randomly generated courses using a genetic algorithm.</p> |
|
|
|
<p>Each time you click "New Track", the course layout and checkpoint locations are randomized. This forces the AI to develop general driving skills rather than memorizing a specific track.</p> |
|
|
|
<p>Key components:</p> |
|
<ul class="list-disc pl-5 space-y-1"> |
|
<li><span class="font-medium">Random Tracks:</span> Procedurally generated with varying complexity</li> |
|
<li><span class="font-medium">Sensors:</span> 5 distance sensors (front, left, right, front-left, front-right)</li> |
|
<li><span class="font-medium">Neural Network:</span> 5 inputs, 1 hidden layer (6 neurons), 2 outputs (left/right)</li> |
|
<li><span class="font-medium">Fitness:</span> Based on distance traveled and checkpoints reached</li> |
|
<li><span class="font-medium">Mutation:</span> Random changes to keep diversity</li> |
|
</ul> |
|
|
|
<div class="pt-2 border-t border-gray-700 mt-4"> |
|
<p class="text-xs text-gray-400">Watch as the AI learns to navigate completely new tracks!</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
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 populationSlider = document.getElementById('populationSlider'); |
|
const mutationSlider = document.getElementById('mutationSlider'); |
|
const populationValue = document.getElementById('populationValue'); |
|
const mutationValue = document.getElementById('mutationValue'); |
|
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 genTime = document.getElementById('genTime'); |
|
const bestProgressBar = document.getElementById('bestProgressBar'); |
|
|
|
|
|
let populationSize = parseInt(populationSlider.value); |
|
let mutationRate = parseInt(mutationSlider.value) / 100; |
|
let isRunning = false; |
|
let generation = 0; |
|
let lastFrameTime = 0; |
|
let fps = 0; |
|
let bestCarProgress = 0; |
|
|
|
|
|
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 => { |
|
ctx.fillRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height); |
|
}); |
|
|
|
|
|
ctx.fillStyle = 'rgba(96, 165, 250, 0.5)'; |
|
ctx.fillRect(this.startPosition.x - 15, this.startPosition.y - 25, 30, 50); |
|
} |
|
}; |
|
|
|
|
|
class Car { |
|
constructor(brain) { |
|
this.reset(); |
|
this.brain = brain ? brain : new NeuralNetwork([5, 6, 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; |
|
} |
|
|
|
reset() { |
|
this.x = track.startPosition.x; |
|
this.y = track.startPosition.y; |
|
this.angle = track.startPosition.angle; |
|
this.speed = 0; |
|
this.maxSpeed = 5; |
|
this.acceleration = 0.1; |
|
this.rotationSpeed = 0.05; |
|
this.damaged = false; |
|
this.checkpointIndex = 0; |
|
this.fitness = 0; |
|
} |
|
|
|
update() { |
|
if (this.damaged) return; |
|
|
|
|
|
this.speed = this.maxSpeed; |
|
this.x += Math.sin(this.angle) * this.speed; |
|
this.y -= Math.cos(this.angle) * this.speed; |
|
|
|
|
|
this.updateSensors(); |
|
|
|
|
|
const outputs = this.brain.predict(this.sensors); |
|
|
|
|
|
const steering = outputs[1] - outputs[0]; |
|
this.angle += steering * this.rotationSpeed; |
|
|
|
|
|
this.checkCollisions(); |
|
|
|
|
|
this.checkCheckpoints(); |
|
|
|
|
|
this.fitness += this.speed; |
|
} |
|
|
|
updateSensors() { |
|
this.sensors = this.sensorAngles.map(angle => { |
|
const sensorAngle = this.angle + angle; |
|
let sensorX = this.x; |
|
let sensorY = this.y; |
|
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() { |
|
|
|
for (const wall of track.walls) { |
|
if (this.x > wall.x && this.x < wall.x + wall.width && |
|
this.y > wall.y && this.y < wall.y + wall.height) { |
|
this.damaged = true; |
|
break; |
|
} |
|
} |
|
|
|
|
|
if (this.x < 0 || this.x > canvas.width || this.y < 0 || this.y > canvas.height) { |
|
this.damaged = true; |
|
} |
|
} |
|
|
|
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}%`; |
|
} |
|
} |
|
} |
|
|
|
draw(ctx) { |
|
if (this.damaged) return; |
|
|
|
ctx.save(); |
|
ctx.translate(this.x, this.y); |
|
ctx.rotate(this.angle); |
|
|
|
|
|
ctx.fillStyle = this.isBest ? 'rgba(220, 38, 38, 0.9)' : this.color; |
|
ctx.fillRect(-6, -10, 12, 20); |
|
|
|
|
|
if (this.isBest) { |
|
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 sensorEndX = Math.sin(sensorAngle) * this.sensorLength * (1 - sensorValue); |
|
const sensorEndY = -Math.cos(sensorAngle) * this.sensorLength * (1 - sensorValue); |
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(0, 0); |
|
ctx.lineTo(sensorEndX, sensorEndY); |
|
ctx.stroke(); |
|
}); |
|
} |
|
|
|
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] |
|
)); |
|
} |
|
} |
|
|
|
static feedForward(givenInputs, network) { |
|
let outputs = Level.feedForward( |
|
givenInputs, network.levels[0] |
|
); |
|
for (let i = 1; i < network.levels.length; i++) { |
|
outputs = Level.feedForward( |
|
outputs, network.levels[i] |
|
); |
|
} |
|
return outputs; |
|
} |
|
|
|
predict(inputs) { |
|
return NeuralNetwork.feedForward(inputs, this); |
|
} |
|
|
|
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 |
|
); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
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] = sum > level.biases[i] ? 1 : 0; |
|
} |
|
|
|
return level.outputs; |
|
} |
|
|
|
clone() { |
|
const clone = new Level(0, 0); |
|
clone.inputs = [...this.inputs]; |
|
clone.outputs = [...this.outputs]; |
|
clone.biases = [...this.biases]; |
|
clone.weights = this.weights.map(arr => [...arr]); |
|
return clone; |
|
} |
|
} |
|
|
|
|
|
function nextGeneration() { |
|
const startTime = performance.now(); |
|
generation++; |
|
generationCount.textContent = generation; |
|
|
|
|
|
track.generateRandomTrack(); |
|
|
|
|
|
calculateFitness(); |
|
|
|
|
|
const newPopulation = []; |
|
|
|
|
|
const bestCar = getBestCar(); |
|
bestCar.isBest = true; |
|
newPopulation.push(bestCar.clone()); |
|
|
|
|
|
for (let i = 1; i < populationSize; i++) { |
|
const parent = selectParent(); |
|
const child = parent.clone(); |
|
child.brain.mutate(mutationRate); |
|
newPopulation.push(child); |
|
} |
|
|
|
|
|
cars = newPopulation; |
|
|
|
|
|
cars.forEach(car => car.reset()); |
|
|
|
|
|
const endTime = performance.now(); |
|
genTime.textContent = Math.round(endTime - startTime); |
|
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 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) { |
|
r -= cars[index].fitness; |
|
index++; |
|
} |
|
|
|
index--; |
|
return cars[index]; |
|
} |
|
|
|
|
|
let cars = []; |
|
let animationId; |
|
let lastTime = 0; |
|
let frameCount = 0; |
|
let lastFpsUpdate = 0; |
|
|
|
|
|
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; |
|
lastTime = performance.now(); |
|
animate(); |
|
} |
|
|
|
|
|
function animate(currentTime = 0) { |
|
if (!isRunning) return; |
|
|
|
animationId = requestAnimationFrame(animate); |
|
|
|
|
|
frameCount++; |
|
if (currentTime - lastFpsUpdate >= 1000) { |
|
fps = Math.round((frameCount * 1000) / (currentTime - lastFpsUpdate)); |
|
fpsCounter.textContent = fps; |
|
frameCount = 0; |
|
lastFpsUpdate = currentTime; |
|
} |
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
track.draw(ctx); |
|
|
|
|
|
let alive = 0; |
|
cars.forEach(car => { |
|
car.update(); |
|
car.draw(ctx); |
|
if (!car.damaged) alive++; |
|
}); |
|
|
|
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; |
|
animate(); |
|
} |
|
}); |
|
|
|
pauseBtn.addEventListener('click', () => { |
|
isRunning = false; |
|
cancelAnimationFrame(animationId); |
|
}); |
|
|
|
resetBtn.addEventListener('click', () => { |
|
isRunning = false; |
|
cancelAnimationFrame(animationId); |
|
init(); |
|
}); |
|
|
|
populationSlider.addEventListener('input', () => { |
|
populationSize = parseInt(populationSlider.value); |
|
populationValue.textContent = populationSize; |
|
}); |
|
|
|
mutationSlider.addEventListener('input', () => { |
|
mutationRate = parseInt(mutationSlider.value) / 100; |
|
mutationValue.textContent = `${parseInt(mutationSlider.value)}%`; |
|
}); |
|
|
|
|
|
init(); |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=lunarflu/https-huggingface-co-spaces-lunarflu-rpg" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |