Evolution-ml / index.html
seawolf2357's picture
Update index.html
a168763 verified
<!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 (Circular Track)</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
<style>
body {
background-color: #111827;
color: #f3f4f6;
font-family: Arial, sans-serif;
line-height: 1.5;
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #60a5fa;
text-align: center;
margin-bottom: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
letter-spacing: 1px;
}
canvas {
background-color: #2d3748;
border-radius: 12px;
display: block;
margin: 0 auto 20px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
border: 2px solid #4a5568;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
justify-content: center;
flex-wrap: wrap;
}
button {
background-color: #3b82f6;
color: white;
border: none;
padding: 10px 18px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 5px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
button:hover {
background-color: #2563eb;
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
button:active {
transform: translateY(1px);
}
.start-btn {
background-color: #10b981;
}
.start-btn:hover {
background-color: #059669;
}
.pause-btn {
background-color: #f59e0b;
}
.pause-btn:hover {
background-color: #d97706;
}
.reset-btn {
background-color: #ef4444;
}
.reset-btn:hover {
background-color: #dc2626;
}
.stats {
display: flex;
justify-content: space-between;
background-color: #1f2937;
padding: 18px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
flex-wrap: wrap;
gap: 10px;
border: 1px solid #374151;
}
.stat-item {
text-align: center;
background-color: #2d3748;
padding: 10px 15px;
border-radius: 8px;
min-width: 90px;
transition: all 0.3s ease;
}
.stat-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 1.4em;
font-weight: bold;
color: #60a5fa;
margin-top: 5px;
}
.settings {
background-color: #1f2937;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #374151;
}
.slider-container {
margin-bottom: 15px;
background-color: #2d3748;
padding: 12px;
border-radius: 8px;
}
.slider-container label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #d1d5db;
}
input[type="range"] {
width: 100%;
margin-bottom: 8px;
height: 6px;
-webkit-appearance: none;
background: #4b5563;
border-radius: 5px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #3b82f6;
border-radius: 50%;
cursor: pointer;
transition: background 0.2s;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: #2563eb;
}
.progress-container {
background-color: #374151;
height: 10px;
border-radius: 5px;
margin-top: 10px;
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #10b981, #3b82f6);
width: 0;
transition: width 0.3s;
box-shadow: 0 0 5px rgba(16, 185, 129, 0.5);
}
/* Car emoji styling */
.car-emoji {
font-size: 1.2em;
margin-right: 5px;
}
/* Added visual elements */
.section-title {
display: flex;
align-items: center;
margin-bottom: 15px;
border-bottom: 1px solid #374151;
padding-bottom: 8px;
}
.section-title i {
margin-right: 8px;
color: #60a5fa;
}
/* Custom checkbox */
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #4b5563;
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #3b82f6;
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
</style>
</head>
<body>
<div class="container">
<h1><i class="fas fa-car-side"></i> Evolution Simulation (Circular Track)</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 on a circular (looped) track using genetic algorithms and neural networks. Cars must navigate this randomly generated circuit without any prior knowledge of the environment.</p>
<p><strong>Key Features:</strong></p>
<ul>
<li><i class="fas fa-brain"></i> <strong>Enhanced Neural Network:</strong> Using Sigmoid activation function</li>
<li><i class="fas fa-random"></i> <strong>Crossover:</strong> Combining best traits from top performers</li>
<li><i class="fas fa-chart-line"></i> <strong>Adaptive Mutation:</strong> Rate adjusts as generations progress</li>
<li><i class="fas fa-bolt"></i> <strong>Performance Optimization:</strong> Delta-time based updates</li>
<li><i class="fas fa-exclamation-triangle"></i> <strong>Improved Collision Detection:</strong> Polygon & line-segment based</li>
<li><i class="fas fa-save"></i> <strong>Model Saving:</strong> Save and load your best models</li>
</ul>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Add roundRect polyfill for browsers that don't support it (optional)
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius) {
if (typeof radius === 'undefined') {
radius = 5;
}
this.beginPath();
this.moveTo(x + radius, y);
this.lineTo(x + width - radius, y);
this.arcTo(x + width, y, x + width, y + radius, radius);
this.lineTo(x + width, y + height - radius);
this.arcTo(x + width, y + height, x + width - radius, y + height, radius);
this.lineTo(x + radius, y + height);
this.arcTo(x, y + height, x, y + height - radius, radius);
this.lineTo(x, y + radius);
this.arcTo(x, y, x + radius, y, radius);
this.closePath();
return this;
};
}
// Canvas 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 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');
// Simulation parameters
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;
// Activation function
const sigmoid = (x) => 1 / (1 + Math.exp(-x));
// -- TRACK DEFINITION (MODIFIED FOR CIRCULAR CIRCUIT) --
const track = {
walls: [], // Now each wall is a line segment: {x1, y1, x2, y2}
checkpoints: [],// Rectangular placeholders for checkpoint detection
startPosition: { x: 100, y: 250, angle: 0 },
generateRandomTrack() {
this.walls = [];
this.checkpoints = [];
// Center of the track
const cx = canvas.width / 2;
const cy = canvas.height / 2;
// Random outer/inner radius
const baseOuterRadius = 180 + Math.random() * 40;
const baseInnerRadius = baseOuterRadius - 40 - Math.random() * 10;
const segments = 36; // Number of line segments to approximate each ring
const outerPoints = [];
const innerPoints = [];
for (let i = 0; i < segments; i++) {
const angle = (2 * Math.PI * i) / segments;
// Optional small random offset for each point
const offsetOuter = Math.random() * 15 - 7.5;
const offsetInner = Math.random() * 10 - 5;
const rOuter = baseOuterRadius + offsetOuter;
const rInner = baseInnerRadius + offsetInner;
// Outer ring coordinates
const xOuter = cx + rOuter * Math.cos(angle);
const yOuter = cy + rOuter * Math.sin(angle);
outerPoints.push({ x: xOuter, y: yOuter });
// Inner ring coordinates
const xInner = cx + rInner * Math.cos(angle);
const yInner = cy + rInner * Math.sin(angle);
innerPoints.push({ x: xInner, y: yInner });
}
// Create line segments for outer and inner rings
for (let i = 0; i < segments; i++) {
const nextIndex = (i + 1) % segments;
// Outer ring wall segment
this.walls.push({
x1: outerPoints[i].x,
y1: outerPoints[i].y,
x2: outerPoints[nextIndex].x,
y2: outerPoints[nextIndex].y
});
// Inner ring wall segment
this.walls.push({
x1: innerPoints[i].x,
y1: innerPoints[i].y,
x2: innerPoints[nextIndex].x,
y2: innerPoints[nextIndex].y
});
}
// Create checkpoints around the mid-radius
const midRadius = (baseOuterRadius + baseInnerRadius) / 2;
const checkpointCount = 5; // Number of checkpoints
for (let i = 0; i < checkpointCount; i++) {
const angle = (2 * Math.PI * i) / checkpointCount;
const cxp = cx + midRadius * Math.cos(angle);
const cyp = cy + midRadius * Math.sin(angle);
const size = 24;
// Store checkpoint as a rectangle for bounding-box collision
this.checkpoints.push({
x: cxp - size / 2,
y: cyp - size / 2,
width: size,
height: size
});
}
// Choose start position near the inner ring at angle=0 (to the right, for example)
// Adjust the angle so the car is tangent to the track
const startAngle = 0;
this.startPosition = {
x: cx + (baseInnerRadius + 10) * Math.cos(startAngle),
y: cy + (baseInnerRadius + 10) * Math.sin(startAngle),
angle: Math.PI / 2 // face "down" (adjust as needed)
};
},
draw(ctx) {
// Draw outer & inner ring segments
ctx.strokeStyle = '#4a5568';
ctx.lineWidth = 3;
this.walls.forEach(wall => {
ctx.beginPath();
ctx.moveTo(wall.x1, wall.y1);
ctx.lineTo(wall.x2, wall.y2);
ctx.stroke();
});
// Draw checkpoints
ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
ctx.strokeStyle = 'rgba(74, 222, 128, 0.7)';
ctx.lineWidth = 1.5;
this.checkpoints.forEach((checkpoint, index) => {
// Draw filled box
ctx.fillRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height);
// Outline
ctx.strokeRect(checkpoint.x, checkpoint.y, checkpoint.width, checkpoint.height);
// Label the checkpoint number
ctx.fillStyle = 'white';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText((index + 1).toString(),
checkpoint.x + checkpoint.width / 2,
checkpoint.y + checkpoint.height / 2);
// Revert fill style for next checkpoint
ctx.fillStyle = 'rgba(74, 222, 128, 0.3)';
});
// Draw start area
ctx.fillStyle = 'rgba(96, 165, 250, 0.5)';
ctx.beginPath();
// Just draw a small rectangle or circle around the start
ctx.arc(this.startPosition.x, this.startPosition.y, 10, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = 'white';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText("START", this.startPosition.x, this.startPosition.y - 15);
}
};
// Car class
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 };
// Update sensors
this.updateSensors();
// Neural net outputs
const outputs = this.brain.predict(this.sensors);
// Steering
const steering = outputs[1] - outputs[0];
this.angle += steering * this.rotationSpeed * dt;
// Move (constant throttle for simplicity)
this.speed = this.maxSpeed;
this.x += Math.sin(this.angle) * this.speed * dt;
this.y -= Math.cos(this.angle) * this.speed * dt;
// Collision check
this.checkCollisions();
// Checkpoints
this.checkCheckpoints();
// Check if car is stuck
const distance = Math.sqrt(
(this.x - this.lastPosition.x) ** 2 +
(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;
const sensorEndX = this.x + Math.sin(sensorAngle) * this.sensorLength;
const sensorEndY = this.y - Math.cos(sensorAngle) * this.sensorLength;
let minDistance = this.sensorLength;
// Check intersection with each wall segment
for (const wall of track.walls) {
const intersection = this.lineSegmentIntersection(
this.x, this.y, sensorEndX, sensorEndY,
wall.x1, wall.y1, wall.x2, wall.y2
);
if (intersection) {
const dist = Math.sqrt(
(intersection.x - this.x) ** 2 +
(intersection.y - this.y) ** 2
);
if (dist < minDistance) {
minDistance = dist;
}
}
}
// Normalized distance
return 1 - (minDistance / this.sensorLength);
});
}
lineSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
// Standard line-line intersection
const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
if (denom === 0) return null; // Parallel
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return {
x: x1 + ua * (x2 - x1),
y: y1 + ua * (y2 - y1)
};
}
return null;
}
checkCollisions() {
// Car polygon (4 corners)
const corners = this.getCarCorners();
// Each side of the car is a line segment
const carEdges = [];
for (let i = 0; i < 4; i++) {
const nextIndex = (i + 1) % 4;
carEdges.push([ corners[i], corners[nextIndex] ]);
}
// Check each car edge vs each wall segment
for (const wall of track.walls) {
for (const edge of carEdges) {
const intersect = this.lineSegmentIntersection(
edge[0].x, edge[0].y, edge[1].x, edge[1].y,
wall.x1, wall.y1, wall.x2, wall.y2
);
if (intersect) {
this.damaged = true;
return;
}
}
}
// Also check if out of canvas bounds (optional)
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 }, // top-left
{ x: width/2, y: -height/2 }, // top-right
{ x: width/2, y: height/2 }, // bottom-right
{ x: -width/2, y: height/2 } // bottom-left
];
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;
// Update global best progress bar
const progress = this.checkpointIndex / track.checkpoints.length;
if (progress > bestCarProgress) {
bestCarProgress = progress;
bestProgressBar.style.width = `${progress * 100}%`;
createConfetti(10, this.x, this.y);
}
}
}
draw(ctx) {
if (this.damaged) return;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
// Draw the car
if (this.isBest) {
// "Best" car in red
ctx.fillStyle = 'rgba(220, 38, 38, 0.9)';
ctx.fillRect(-6, -10, 12, 20);
// Wheels
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);
// Windshield
ctx.fillStyle = '#60a5fa';
ctx.fillRect(-4, -8, 8, 6);
// Simple crown on top
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.moveTo(-3, -11);
ctx.lineTo(-1, -13);
ctx.lineTo(1, -11);
ctx.lineTo(3, -13);
ctx.lineTo(3, -10);
ctx.lineTo(-3, -10);
ctx.fill();
} else {
// Normal car
ctx.fillStyle = this.color;
ctx.fillRect(-6, -10, 12, 20);
// Wheels
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);
// Windshield
ctx.fillStyle = '#a3e0ff';
ctx.fillRect(-4, -7, 8, 5);
}
// If it's best car, draw sensors after restoring context
if (this.isBest) {
ctx.restore();
// Draw sensor lines
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 length = this.sensorLength * (1 - sensorValue);
const endX = this.x + Math.sin(sensorAngle) * length;
const endY = this.y - Math.cos(sensorAngle) * length;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(endX, endY);
ctx.stroke();
});
return;
}
ctx.restore();
}
clone() {
return new Car(this.brain.clone());
}
}
// Neural Network classes
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("Parent network structures differ!");
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("Parent level structures differ!");
return parentA.clone();
}
const childLevel = new Level(levelA.inputs.length, levelA.outputs.length);
// Single-point crossover for biases & weights
const biasesSwitch = Math.floor(Math.random() * levelA.biases.length);
for (let i = 0; i < childLevel.biases.length; i++) {
childLevel.biases[i] = i < biasesSwitch
? levelA.biases[i]
: levelB.biases[i];
}
for (let i = 0; i < childLevel.weights.length; i++) {
const weightSwitch = Math.floor(Math.random() * levelA.weights[i].length);
for (let j = 0; j < childLevel.weights[i].length; j++) {
childLevel.weights[i][j] = j < weightSwitch
? levelA.weights[i][j]
: levelB.weights[i][j];
}
}
childNetwork.levels.push(childLevel);
}
return childNetwork;
}
toJSON() {
return {
levels: this.levels.map(level => ({
inputs: level.inputs,
outputs: level.outputs,
biases: level.biases,
weights: level.weights
}))
};
}
static fromJSON(data) {
const network = new NeuralNetwork([]);
network.levels = data.levels.map(levelData => {
const level = new Level(levelData.inputs.length, levelData.outputs.length);
level.inputs = [...levelData.inputs];
level.outputs = [...levelData.outputs];
level.biases = [...levelData.biases];
level.weights = levelData.weights.map(w => [...w]);
return level;
});
return network;
}
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
class Level {
constructor(inputCount, outputCount) {
this.inputs = new Array(inputCount);
this.outputs = new Array(outputCount);
this.biases = new Array(outputCount);
this.weights = [];
for (let i = 0; i < inputCount; i++) {
this.weights[i] = new Array(outputCount);
}
Level.randomize(this);
}
static randomize(level) {
for (let i = 0; i < level.inputs.length; i++) {
for (let j = 0; j < level.outputs.length; j++) {
level.weights[i][j] = Math.random() * 2 - 1;
}
}
for (let i = 0; i < level.biases.length; i++) {
level.biases[i] = Math.random() * 2 - 1;
}
}
static feedForward(givenInputs, level) {
for (let i = 0; i < level.inputs.length; i++) {
level.inputs[i] = givenInputs[i];
}
for (let i = 0; i < level.outputs.length; i++) {
let sum = 0;
for (let j = 0; j < level.inputs.length; j++) {
sum += level.inputs[j] * level.weights[j][i];
}
// Sigmoid
level.outputs[i] = sigmoid(sum - level.biases[i]);
}
return level.outputs;
}
clone() {
const clone = new Level(this.inputs.length, this.outputs.length);
clone.inputs = [...this.inputs];
clone.outputs = [...this.outputs];
clone.biases = [...this.biases];
clone.weights = this.weights.map(arr => [...arr]);
return clone;
}
}
// Genetic Algorithm
function nextGeneration() {
generation++;
generationCount.textContent = generation;
// Compute fitness
calculateFitness();
mutationValue.textContent = `${Math.round(mutationRate * 100)}%`;
// Create new population
const newPopulation = [];
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) {
// Crossover
const parentA = selectParent();
const parentB = selectParent();
const child = new Car(NeuralNetwork.crossover(parentA.brain, parentB.brain));
child.brain.mutate(mutationRate);
newPopulation.push(child);
} else {
// Mutation only
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() {
// Roulette wheel
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];
}
// Save/load model
function saveBestModel() {
const bestCar = getBestCar();
if (bestCar) {
try {
const modelData = {
brain: bestCar.brain.toJSON(),
fitness: bestCar.fitness,
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 restoredBrain = NeuralNetwork.fromJSON(modelData.brain);
// Create new population from loaded model
const newPopulation = [];
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;
}
// Confetti
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,
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 p = confetti[i];
p.x += p.vx * dt * 60;
p.y += p.vy * dt * 60;
p.vy += p.gravity * dt * 60;
p.life -= 0.016 * dt * 60;
if (p.life <= 0) {
confetti.splice(i, 1);
}
}
}
function drawConfetti(ctx) {
for (const p of confetti) {
ctx.fillStyle = p.color;
ctx.globalAlpha = p.life;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.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);
// Show victory message
const message = document.createElement('div');
message.style.position = 'absolute';
message.style.top = '50%';
message.style.left = '50%';
message.style.transform = 'translate(-50%, -50%)';
message.style.background = 'rgba(16, 185, 129, 0.9)';
message.style.color = 'white';
message.style.padding = '20px';
message.style.borderRadius = '10px';
message.style.fontSize = '24px';
message.style.fontWeight = 'bold';
message.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
message.style.zIndex = '1000';
message.style.textAlign = 'center';
message.innerHTML = `
<div><i class="fas fa-trophy"></i> Course Completed! <i class="fas fa-trophy"></i></div>
<div style="font-size: 16px; margin-top: 10px;">
Generations: ${generation}<br>
Fitness: ${Math.round(bestCar.fitness * 1000)}
</div>
<button id="continueBtn" style="
background-color: white;
color: #10b981;
border: none;
padding: 8px 16px;
border-radius: 5px;
margin-top: 15px;
cursor: pointer;
font-weight: bold;">
Continue Training
</button>
`;
document.body.appendChild(message);
isRunning = false;
cancelAnimationFrame(animationId);
document.getElementById('continueBtn').addEventListener('click', () => {
document.body.removeChild(message);
isRunning = true;
window.courseCompleted = false;
lastUpdateTime = performance.now();
animate();
});
}
}
// Simulation state
let cars = [];
let animationId;
// Initialize
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);
// FPS
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;
}
// Clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw track
track.draw(ctx);
// Update & draw cars
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();
}
}
// Buttons
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');
}
});
// Sliders
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`;
});
// Start
init();
});
</script>
</body>
</html>