| | <!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> |