lunarflu's picture
lunarflu HF Staff
Add 1 files
0c42f4b verified
<!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', () => {
// Canvas setup
const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');
// UI elements
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');
// Simulation parameters
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;
// Track definition
const track = {
walls: [],
checkpoints: [],
startPosition: { x: 100, y: 250, angle: 0 },
generateRandomTrack() {
this.walls = [];
this.checkpoints = [];
// Outer boundary walls (always present)
this.walls.push(
{ x: 50, y: 50, width: 700, height: 20 }, // top
{ x: 50, y: 50, width: 20, height: 400 }, // left
{ x: 50, y: 430, width: 700, height: 20 }, // bottom
{ x: 730, y: 50, width: 20, height: 400 } // right
);
// Generate random obstacles (between 3-8 obstacles)
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;
}
// Make sure obstacle doesn't block the start position
if (!(x < 150 && y < 300 && y + height > 200)) {
this.walls.push({ x, y, width, height });
}
}
// Generate checkpoints (3-5 checkpoints)
const checkpointCount = 3 + Math.floor(Math.random() * 3);
const checkpointSize = 30;
// Generate positions that require navigating around obstacles
const possiblePositions = [
{ x: 700, y: 100 }, // right side top
{ x: 600, y: 400 }, // middle right bottom
{ x: 300, y: 400 }, // middle bottom
{ x: 100, y: 300 }, // left side middle
{ x: 400, y: 100 }, // middle top
{ x: 200, y: 200 }, // middle left
{ x: 600, y: 200 } // middle right
];
// Shuffle and take first checkpointCount positions
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
});
}
// Set start position (always left side, but random vertical position)
this.startPosition = {
x: 100,
y: 100 + Math.random() * 300,
angle: 0
};
},
draw(ctx) {
// Draw walls
ctx.fillStyle = '#4a5568';
this.walls.forEach(wall => {
ctx.fillRect(wall.x, wall.y, wall.width, wall.height);
});
// Draw checkpoints
ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
this.checkpoints.forEach(checkpoint => {
ctx.fillRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height);
});
// Draw start position
ctx.fillStyle = 'rgba(96, 165, 250, 0.5)';
ctx.fillRect(this.startPosition.x - 15, this.startPosition.y - 25, 30, 50);
}
};
// Car class
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]; // Front, left, right, front-left, front-right
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;
// Update position
this.speed = this.maxSpeed;
this.x += Math.sin(this.angle) * this.speed;
this.y -= Math.cos(this.angle) * this.speed;
// Update sensors
this.updateSensors();
// Get neural network outputs
const outputs = this.brain.predict(this.sensors);
// Apply steering (outputs[0] = left, outputs[1] = right)
const steering = outputs[1] - outputs[0]; // -1 to 1
this.angle += steering * this.rotationSpeed;
// Check collisions
this.checkCollisions();
// Check checkpoints
this.checkCheckpoints();
// Update fitness
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;
// Check against all walls
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);
}
}
// Normalize distance to 0-1 range (1 = no obstacle, 0 = obstacle at car)
return 1 - (minDistance / this.sensorLength);
});
}
lineRectIntersection(x1, y1, x2, y2, rx, ry, rw, rh) {
// Check if the line has hit any of the rectangle's sides
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) {
// Calculate the intersection point between two lines
const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
if (denominator === 0) return null; // Lines are parallel
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() {
// Simple collision detection with walls
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;
}
}
// Boundary checks
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; // Bonus for reaching checkpoint
// Update best progress
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);
// Draw car body
ctx.fillStyle = this.isBest ? 'rgba(220, 38, 38, 0.9)' : this.color;
ctx.fillRect(-6, -10, 12, 20);
// Draw sensors (for best car)
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());
}
}
// Neural Network class
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;
}
}
// Genetic algorithm functions
function nextGeneration() {
const startTime = performance.now();
generation++;
generationCount.textContent = generation;
// lunarflu
track.generateRandomTrack();
// Calculate fitness
calculateFitness();
// Create new population
const newPopulation = [];
// Add the best car from previous generation (elitism)
const bestCar = getBestCar();
bestCar.isBest = true;
newPopulation.push(bestCar.clone());
// Fill the rest with crossover and mutation
for (let i = 1; i < populationSize; i++) {
const parent = selectParent();
const child = parent.clone();
child.brain.mutate(mutationRate);
newPopulation.push(child);
}
// Replace old population
cars = newPopulation;
// Reset cars
cars.forEach(car => car.reset());
// Update UI
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 => {
// Add bonus for checkpoints reached
car.fitness += car.checkpointIndex * 500;
sum += car.fitness;
if (car.fitness > max) max = car.fitness;
});
// Normalize 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];
}
// Simulation state
let cars = [];
let animationId;
let lastTime = 0;
let frameCount = 0;
let lastFpsUpdate = 0;
// Initialize simulation
function init() {
// Generate random track
track.generateRandomTrack();
// Create initial population
cars = [];
for (let i = 0; i < populationSize; i++) {
cars.push(new Car());
}
generation = 0;
generationCount.textContent = generation;
populationCount.textContent = populationSize;
// Start simulation
isRunning = true;
lastTime = performance.now();
animate();
}
// Main animation loop
function animate(currentTime = 0) {
if (!isRunning) return;
animationId = requestAnimationFrame(animate);
// Calculate FPS
frameCount++;
if (currentTime - lastFpsUpdate >= 1000) {
fps = Math.round((frameCount * 1000) / (currentTime - lastFpsUpdate));
fpsCounter.textContent = fps;
frameCount = 0;
lastFpsUpdate = currentTime;
}
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw track
track.draw(ctx);
// Update and draw cars
let alive = 0;
cars.forEach(car => {
car.update();
car.draw(ctx);
if (!car.damaged) alive++;
});
aliveCount.textContent = alive;
// Check if all cars are dead
if (alive === 0) {
nextGeneration();
}
// Highlight best car
const bestCar = getBestCar();
if (bestCar) {
bestCar.isBest = true;
bestCar.color = 'rgba(220, 38, 38, 0.9)';
}
}
// Event listeners
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)}%`;
});
// Initialize the simulation
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>