SimDrivers / index.html
awacke1's picture
Update index.html
16fe8d4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enhanced Road-Following AI Evolution Simulator</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
background: #000;
}
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
font-size: 14px;
min-width: 200px;
}
#controls {
position: absolute;
top: 10px;
right: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 8px 16px;
margin: 5px;
cursor: pointer;
border-radius: 4px;
font-size: 12px;
}
button:hover {
background-color: #45a049;
}
#stats {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
font-size: 12px;
min-width: 200px;
}
#roadStats {
position: absolute;
bottom: 10px;
right: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
font-size: 12px;
min-width: 180px;
}
#behaviorStats {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
font-size: 12px;
min-width: 180px;
}
.highlight { color: #ffcc00; font-weight: bold; }
.success { color: #00ff00; font-weight: bold; }
.road { color: #00aaff; }
.traveling { color: #ff8800; }
.stopped { color: #ff00ff; font-weight: bold; }
.navigating { color: #00ffff; }
.following { color: #88ff88; }
.progress-bar {
width: 100%;
height: 10px;
background-color: #333;
border-radius: 5px;
overflow: hidden;
margin: 5px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1);
transition: width 0.3s ease;
}
</style>
</head>
<body>
<div id="ui">
<div class="highlight">Road-Following AI Evolution</div>
<div>Epoch: <span id="epoch">1</span></div>
<div>Time: <span id="epochTime">60</span>s</div>
<div class="progress-bar"><div class="progress-fill" id="timeProgress"></div></div>
<div>Population: <span id="population">100</span></div>
<div>Best Fitness: <span id="bestFitness">0</span></div>
<div>Road Mastery: <span id="roadMastery">0</span>%</div>
<div>Navigation IQ: <span id="navigationIQ">50</span></div>
</div>
<div id="controls">
<button id="pauseBtn">Pause</button>
<button id="resetBtn">Reset</button>
<button id="speedBtn">Speed: 1x</button>
<button id="viewBtn">View: Follow</button>
<button id="pathBtn">Paths: ON</button>
<button id="debugBtn">Debug: OFF</button>
</div>
<div id="stats">
<div><span class="highlight">Performance Metrics:</span></div>
<div>Destinations Reached: <span id="destinationsReached">0</span></div>
<div>Road Usage: <span id="roadUsage">0</span>%</div>
<div>Formation Quality: <span id="formationQuality">0</span>%</div>
<div>Path Efficiency: <span id="pathEfficiency">0</span>%</div>
<div>Stop Compliance: <span id="stopCompliance">0</span>%</div>
<div>Traffic Violations: <span id="trafficViolations">0</span></div>
</div>
<div id="roadStats">
<div><span class="highlight">Road Behavior:</span></div>
<div><span class="road">On Roads:</span> <span id="onRoadCount">0</span></div>
<div><span class="traveling">Traveling:</span> <span id="travelingCount">0</span></div>
<div><span class="stopped">Stopped:</span> <span id="stoppedCount">0</span></div>
<div><span class="navigating">Navigating:</span> <span id="navigatingCount">0</span></div>
<div>Single File Lines: <span id="singleFileCount">0</span></div>
<div>Avg Line Length: <span id="avgLineLength">0</span></div>
<div>Traffic Flow: <span id="trafficFlow">0</span>%</div>
</div>
<div id="behaviorStats">
<div><span class="highlight">AI Behaviors:</span></div>
<div>Path Planning: <span id="pathPlanning">0</span>%</div>
<div>Road Following: <span id="roadFollowing">0</span>%</div>
<div>Formation Control: <span id="formationControl">0</span>%</div>
<div>Navigation Skills: <span id="navigationSkills">0</span>%</div>
<div style="margin-top: 10px;"><span class="highlight">Learning Progress:</span></div>
<div>Exploration: <span id="explorationRate">0</span>%</div>
<div>Adaptation: <span id="adaptationRate">0</span>%</div>
<div>Specialization: <span id="specializationRate">0</span>%</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Global variables
let scene, camera, renderer, clock;
let world = {
roads: [],
intersections: [],
buildings: [],
pathNetwork: new Map(),
roadSegments: []
};
// Evolution system
let epoch = 1;
let epochTime = 90; // Longer epochs for complex behaviors
let timeLeft = 90;
let population = [];
let populationSize = 80; // Smaller population for more complex AI
let bestFitness = 0;
let paused = false;
let speedMultiplier = 1;
let cameraMode = 'follow';
let showPaths = true;
let debugMode = false;
// Road network constants
const ROAD_WIDTH = 12;
const ROAD_SPACING = 120;
const INTERSECTION_SIZE = 15;
const LANE_WIDTH = 6;
const BUILDING_COUNT = 16;
// Enhanced Neural Network for Road Navigation
class RoadNavigationBrain {
constructor() {
this.inputSize = 32; // Expanded for road navigation
this.hiddenLayers = [48, 36, 24]; // Deeper network
this.outputSize = 12; // More nuanced control
this.weights = [];
this.biases = [];
this.memory = new Array(8).fill(0); // Road memory
this.pathMemory = []; // Remember planned paths
this.initializeNetwork();
// Specialized road behaviors
this.roadFollowingStrength = Math.random();
this.formationPreference = Math.random();
this.navigationSkill = Math.random();
this.pathPlanningAbility = Math.random();
this.stopDiscipline = Math.random();
}
initializeNetwork() {
let prevSize = this.inputSize + this.memory.length;
for (let i = 0; i < this.hiddenLayers.length; i++) {
this.weights.push(this.randomMatrix(prevSize, this.hiddenLayers[i]));
this.biases.push(this.randomArray(this.hiddenLayers[i]));
prevSize = this.hiddenLayers[i];
}
this.weights.push(this.randomMatrix(prevSize, this.outputSize));
this.biases.push(this.randomArray(this.outputSize));
}
randomMatrix(rows, cols) {
return Array(rows).fill().map(() =>
Array(cols).fill().map(() => (Math.random() - 0.5) * 2)
);
}
randomArray(size) {
return Array(size).fill().map(() => (Math.random() - 0.5) * 2);
}
activate(inputs) {
let currentInput = [...inputs, ...this.memory];
for (let layer = 0; layer < this.hiddenLayers.length; layer++) {
currentInput = this.forwardLayer(currentInput, this.weights[layer], this.biases[layer]);
}
const outputs = this.forwardLayer(currentInput,
this.weights[this.weights.length - 1],
this.biases[this.biases.length - 1]);
// Update memory with important road information
this.updateMemory(inputs);
return outputs;
}
forwardLayer(inputs, weights, biases) {
const outputs = new Array(weights[0].length).fill(0);
for (let i = 0; i < outputs.length; i++) {
for (let j = 0; j < inputs.length; j++) {
outputs[i] += inputs[j] * weights[j][i];
}
outputs[i] += biases[i];
outputs[i] = this.activationFunction(outputs[i]);
}
return outputs;
}
activationFunction(x) {
return Math.tanh(Math.max(-10, Math.min(10, x)));
}
updateMemory(inputs) {
// Store road-following information
const roadInfo = inputs.slice(16, 20); // Road detection inputs
this.memory = [...roadInfo, ...this.memory.slice(0, 4)];
}
mutate(rate = 0.1) {
this.weights.forEach(matrix => {
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
if (Math.random() < rate) {
matrix[i][j] += (Math.random() - 0.5) * 0.5;
matrix[i][j] = Math.max(-3, Math.min(3, matrix[i][j]));
}
}
}
});
this.biases.forEach(bias => {
for (let i = 0; i < bias.length; i++) {
if (Math.random() < rate) {
bias[i] += (Math.random() - 0.5) * 0.5;
bias[i] = Math.max(-3, Math.min(3, bias[i]));
}
}
});
// Mutate behavioral traits
if (Math.random() < rate) {
this.roadFollowingStrength += (Math.random() - 0.5) * 0.2;
this.roadFollowingStrength = Math.max(0, Math.min(1, this.roadFollowingStrength));
}
if (Math.random() < rate) {
this.formationPreference += (Math.random() - 0.5) * 0.2;
this.formationPreference = Math.max(0, Math.min(1, this.formationPreference));
}
}
copy() {
const newBrain = new RoadNavigationBrain();
newBrain.weights = this.weights.map(matrix => matrix.map(row => [...row]));
newBrain.biases = this.biases.map(bias => [...bias]);
newBrain.memory = [...this.memory];
newBrain.roadFollowingStrength = this.roadFollowingStrength;
newBrain.formationPreference = this.formationPreference;
newBrain.navigationSkill = this.navigationSkill;
newBrain.pathPlanningAbility = this.pathPlanningAbility;
newBrain.stopDiscipline = this.stopDiscipline;
return newBrain;
}
crossover(other) {
const child = new RoadNavigationBrain();
for (let layer = 0; layer < this.weights.length; layer++) {
for (let i = 0; i < this.weights[layer].length; i++) {
for (let j = 0; j < this.weights[layer][i].length; j++) {
child.weights[layer][i][j] = Math.random() < 0.5 ?
this.weights[layer][i][j] : other.weights[layer][i][j];
}
}
for (let i = 0; i < this.biases[layer].length; i++) {
child.biases[layer][i] = Math.random() < 0.5 ?
this.biases[layer][i] : other.biases[layer][i];
}
}
// Blend behavioral traits
child.roadFollowingStrength = (this.roadFollowingStrength + other.roadFollowingStrength) / 2;
child.formationPreference = (this.formationPreference + other.formationPreference) / 2;
child.navigationSkill = (this.navigationSkill + other.navigationSkill) / 2;
child.pathPlanningAbility = (this.pathPlanningAbility + other.pathPlanningAbility) / 2;
child.stopDiscipline = (this.stopDiscipline + other.stopDiscipline) / 2;
return child;
}
}
// Enhanced AI Car with Road Navigation
class RoadNavigationCar {
constructor(x = 0, z = 0) {
this.brain = new RoadNavigationBrain();
this.mesh = this.createCarMesh();
this.mesh.position.set(x, 1, z);
// Movement properties
this.velocity = new THREE.Vector3(0, 0, 0);
this.acceleration = new THREE.Vector3();
this.maxSpeed = 20;
this.minSpeed = 2;
this.targetSpeed = 15;
// Road navigation state
this.currentRoad = null;
this.roadPosition = 0.5; // Position on road (0-1)
this.roadDirection = new THREE.Vector3(1, 0, 0);
this.targetDestination = null;
this.currentPath = [];
this.pathIndex = 0;
this.state = 'seeking'; // seeking, traveling, stopped, following
// Formation and following
this.leader = null;
this.followers = [];
this.formationPosition = 0;
this.targetFollowingDistance = 8;
// Timing and stops
this.stopTimer = 0;
this.stopDuration = 10; // 10 seconds
this.lastDestinationTime = 0;
// Fitness tracking
this.fitness = 0;
this.roadUsageScore = 0;
this.destinationsReached = 0;
this.formationScore = 0;
this.pathEfficiencyScore = 0;
this.stopComplianceScore = 0;
this.trafficViolations = 0;
this.distanceTraveled = 0;
// Sensors
this.sensors = {
obstacles: Array(8).fill(0),
roads: Array(4).fill(0),
traffic: Array(6).fill(0),
destinations: Array(4).fill(0),
formation: Array(4).fill(0),
navigation: Array(6).fill(0)
};
this.initializeNavigation();
this.createVisualization();
}
createCarMesh() {
const group = new THREE.Group();
// Car body
const bodyGeometry = new THREE.BoxGeometry(1.5, 0.8, 3);
this.bodyMaterial = new THREE.MeshLambertMaterial({
color: new THREE.Color().setHSL(Math.random(), 0.8, 0.6)
});
const body = new THREE.Mesh(bodyGeometry, this.bodyMaterial);
body.position.y = 0.4;
body.castShadow = true;
group.add(body);
// State indicator
this.stateIndicator = new THREE.Mesh(
new THREE.SphereGeometry(0.3, 8, 6),
new THREE.MeshLambertMaterial({ color: 0x00ff00 })
);
this.stateIndicator.position.set(0, 1.5, 0);
group.add(this.stateIndicator);
// Direction arrow
const arrowGeometry = new THREE.ConeGeometry(0.2, 0.8, 6);
this.directionArrow = new THREE.Mesh(arrowGeometry,
new THREE.MeshLambertMaterial({ color: 0xffffff }));
this.directionArrow.position.set(0, 2, 0);
this.directionArrow.rotation.x = -Math.PI / 2;
group.add(this.directionArrow);
return group;
}
createVisualization() {
// Path visualization with proper geometry initialization
const pathGeometry = new THREE.BufferGeometry();
// Initialize with a basic line to prevent undefined geometry
pathGeometry.setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, 0)
]);
this.pathLine = new THREE.Line(
pathGeometry,
new THREE.LineBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.6
})
);
this.pathLine.visible = showPaths;
scene.add(this.pathLine);
// Destination marker
this.destinationMarker = new THREE.Mesh(
new THREE.RingGeometry(2, 3, 8),
new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.5
})
);
this.destinationMarker.rotation.x = -Math.PI / 2;
this.destinationMarker.visible = false;
scene.add(this.destinationMarker);
}
initializeNavigation() {
// Delay initial destination selection to ensure world is ready
setTimeout(() => {
this.selectNewDestination();
this.mesh.rotation.y = Math.random() * Math.PI * 2;
}, 100);
}
selectNewDestination() {
if (!world.buildings || world.buildings.length === 0) {
console.warn('No buildings available for navigation');
return;
}
// Choose a random building different from current position
let targetBuilding;
let attempts = 0;
do {
targetBuilding = world.buildings[Math.floor(Math.random() * world.buildings.length)];
attempts++;
} while (this.targetDestination &&
targetBuilding.position.distanceTo(this.targetDestination) < 50 &&
attempts < 10); // Prevent infinite loop
this.targetDestination = targetBuilding.position.clone();
this.planPath();
this.state = 'seeking';
this.lastDestinationTime = Date.now();
// Update destination marker
if (this.destinationMarker) {
this.destinationMarker.position.copy(this.targetDestination);
this.destinationMarker.position.y = 0.1;
this.destinationMarker.visible = true;
}
}
planPath() {
if (!this.targetDestination) return;
try {
// Simple pathfinding using road network
const startPos = this.mesh.position.clone();
const endPos = this.targetDestination.clone();
// Find nearest road intersections
const startIntersection = this.findNearestIntersection(startPos);
const endIntersection = this.findNearestIntersection(endPos);
if (startIntersection && endIntersection) {
this.currentPath = this.findPathBetweenIntersections(startIntersection, endIntersection);
} else {
// Direct path if no road network available
this.currentPath = [startPos, endPos];
}
this.pathIndex = 0;
this.updatePathVisualization();
} catch (error) {
console.warn('Path planning error:', error);
// Fallback to direct path
this.currentPath = [this.mesh.position.clone(), this.targetDestination.clone()];
this.pathIndex = 0;
}
}
findNearestIntersection(pos) {
let nearest = null;
let minDist = Infinity;
for (let x = -300; x <= 300; x += ROAD_SPACING) {
for (let z = -300; z <= 300; z += ROAD_SPACING) {
const intersection = new THREE.Vector3(x, 0, z);
const dist = pos.distanceTo(intersection);
if (dist < minDist) {
minDist = dist;
nearest = intersection;
}
}
}
return nearest;
}
findPathBetweenIntersections(start, end) {
// Simple A* pathfinding on grid
const path = [];
const current = start.clone();
while (current.distanceTo(end) > ROAD_SPACING / 2) {
path.push(current.clone());
const dx = end.x - current.x;
const dz = end.z - current.z;
if (Math.abs(dx) > Math.abs(dz)) {
current.x += Math.sign(dx) * ROAD_SPACING;
} else {
current.z += Math.sign(dz) * ROAD_SPACING;
}
}
path.push(end.clone());
return path;
}
updatePathVisualization() {
if (!this.pathLine || !this.pathLine.geometry || !showPaths || this.currentPath.length < 2) {
if (this.pathLine) this.pathLine.visible = false;
return;
}
try {
const points = this.currentPath.map(p => {
const point = p.clone();
point.y = 3;
return point;
});
this.pathLine.geometry.setFromPoints(points);
this.pathLine.visible = true;
} catch (error) {
console.warn('Path visualization error:', error);
if (this.pathLine) this.pathLine.visible = false;
}
}
updateSensors() {
this.updateObstacleSensors();
this.updateRoadSensors();
this.updateTrafficSensors();
this.updateDestinationSensors();
this.updateFormationSensors();
this.updateNavigationSensors();
}
updateObstacleSensors() {
const directions = [
[0, 0, 1], [0.7, 0, 0.7], [1, 0, 0], [0.7, 0, -0.7],
[0, 0, -1], [-0.7, 0, -0.7], [-1, 0, 0], [-0.7, 0, 0.7]
];
const raycaster = new THREE.Raycaster();
directions.forEach((dir, i) => {
const direction = new THREE.Vector3(...dir);
direction.applyQuaternion(this.mesh.quaternion);
raycaster.set(this.mesh.position, direction);
const intersects = raycaster.intersectObjects(this.getObstacles(), true);
if (intersects.length > 0 && intersects[0].distance <= 12) {
this.sensors.obstacles[i] = 1 - (intersects[0].distance / 12);
} else {
this.sensors.obstacles[i] = 0;
}
});
}
updateRoadSensors() {
const pos = this.mesh.position;
// Detect road in 4 directions
this.sensors.roads[0] = this.detectRoad(pos, 'north');
this.sensors.roads[1] = this.detectRoad(pos, 'east');
this.sensors.roads[2] = this.detectRoad(pos, 'south');
this.sensors.roads[3] = this.detectRoad(pos, 'west');
// Update current road status
this.updateCurrentRoad();
}
detectRoad(pos, direction) {
const roadStrength = this.getRoadStrength(pos);
if (roadStrength === 0) return 0;
// Check road direction alignment
const directionVector = this.getDirectionVector(direction);
const roadDir = this.getRoadDirection(pos);
if (roadDir) {
const alignment = Math.abs(directionVector.dot(roadDir));
return roadStrength * alignment;
}
return roadStrength;
}
getRoadStrength(pos) {
const roadWidth = ROAD_WIDTH;
// Check horizontal roads
const nearestHorizontalRoad = Math.round(pos.z / ROAD_SPACING) * ROAD_SPACING;
const distToHorizontalRoad = Math.abs(pos.z - nearestHorizontalRoad);
const onHorizontalRoad = distToHorizontalRoad <= roadWidth / 2;
// Check vertical roads
const nearestVerticalRoad = Math.round(pos.x / ROAD_SPACING) * ROAD_SPACING;
const distToVerticalRoad = Math.abs(pos.x - nearestVerticalRoad);
const onVerticalRoad = distToVerticalRoad <= roadWidth / 2;
if (onHorizontalRoad || onVerticalRoad) {
const hStrength = onHorizontalRoad ? 1 - (distToHorizontalRoad / (roadWidth / 2)) : 0;
const vStrength = onVerticalRoad ? 1 - (distToVerticalRoad / (roadWidth / 2)) : 0;
return Math.max(hStrength, vStrength);
}
return 0;
}
getRoadDirection(pos) {
const roadWidth = ROAD_WIDTH;
const nearestHorizontalRoad = Math.round(pos.z / ROAD_SPACING) * ROAD_SPACING;
const distToHorizontalRoad = Math.abs(pos.z - nearestHorizontalRoad);
const onHorizontalRoad = distToHorizontalRoad <= roadWidth / 2;
const nearestVerticalRoad = Math.round(pos.x / ROAD_SPACING) * ROAD_SPACING;
const distToVerticalRoad = Math.abs(pos.x - nearestVerticalRoad);
const onVerticalRoad = distToVerticalRoad <= roadWidth / 2;
if (onHorizontalRoad && onVerticalRoad) {
// At intersection, prefer current movement direction
return this.velocity.length() > 0 ?
this.velocity.clone().normalize() : new THREE.Vector3(1, 0, 0);
} else if (onHorizontalRoad) {
return new THREE.Vector3(1, 0, 0);
} else if (onVerticalRoad) {
return new THREE.Vector3(0, 0, 1);
}
return null;
}
getDirectionVector(direction) {
switch (direction) {
case 'north': return new THREE.Vector3(0, 0, 1);
case 'east': return new THREE.Vector3(1, 0, 0);
case 'south': return new THREE.Vector3(0, 0, -1);
case 'west': return new THREE.Vector3(-1, 0, 0);
default: return new THREE.Vector3(1, 0, 0);
}
}
updateCurrentRoad() {
const roadStrength = this.getRoadStrength(this.mesh.position);
const roadDir = this.getRoadDirection(this.mesh.position);
if (roadStrength > 0.5 && roadDir) {
this.currentRoad = {
strength: roadStrength,
direction: roadDir
};
this.roadDirection = roadDir;
} else {
this.currentRoad = null;
}
}
updateTrafficSensors() {
const nearbyVehicles = population.filter(car =>
car !== this &&
this.mesh.position.distanceTo(car.mesh.position) < 25
);
// Traffic in 6 directions (front, back, left, right, front-left, front-right)
const directions = [
new THREE.Vector3(0, 0, 1), // front
new THREE.Vector3(0, 0, -1), // back
new THREE.Vector3(-1, 0, 0), // left
new THREE.Vector3(1, 0, 0), // right
new THREE.Vector3(-0.7, 0, 0.7), // front-left
new THREE.Vector3(0.7, 0, 0.7) // front-right
];
directions.forEach((dir, i) => {
const worldDir = dir.clone().applyQuaternion(this.mesh.quaternion);
let trafficDensity = 0;
nearbyVehicles.forEach(car => {
const toVehicle = car.mesh.position.clone().sub(this.mesh.position).normalize();
const alignment = worldDir.dot(toVehicle);
if (alignment > 0.5) {
const distance = this.mesh.position.distanceTo(car.mesh.position);
trafficDensity += Math.max(0, 1 - distance / 25);
}
});
this.sensors.traffic[i] = Math.min(trafficDensity, 1);
});
}
updateDestinationSensors() {
if (!this.targetDestination) {
this.sensors.destinations.fill(0);
return;
}
const toDestination = this.targetDestination.clone().sub(this.mesh.position);
const distance = toDestination.length();
const direction = toDestination.normalize();
// Convert to local coordinates
const localDirection = direction.clone();
localDirection.applyQuaternion(this.mesh.quaternion.clone().invert());
// Destination sensors: front, back, left, right
this.sensors.destinations[0] = Math.max(0, localDirection.z) / Math.sqrt(distance / 100 + 1);
this.sensors.destinations[1] = Math.max(0, -localDirection.z) / Math.sqrt(distance / 100 + 1);
this.sensors.destinations[2] = Math.max(0, -localDirection.x) / Math.sqrt(distance / 100 + 1);
this.sensors.destinations[3] = Math.max(0, localDirection.x) / Math.sqrt(distance / 100 + 1);
}
updateFormationSensors() {
// Single-file formation awareness
const nearbyVehicles = population.filter(car =>
car !== this &&
this.mesh.position.distanceTo(car.mesh.position) < 20
);
let frontVehicle = null;
let backVehicle = null;
let frontDistance = Infinity;
let backDistance = Infinity;
const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
nearbyVehicles.forEach(car => {
const toVehicle = car.mesh.position.clone().sub(this.mesh.position);
const dot = forward.dot(toVehicle.normalize());
const distance = toVehicle.length();
if (dot > 0.8 && distance < frontDistance) {
frontVehicle = car;
frontDistance = distance;
} else if (dot < -0.8 && distance < backDistance) {
backVehicle = car;
backDistance = distance;
}
});
this.sensors.formation[0] = frontVehicle ? Math.max(0, 1 - frontDistance / 15) : 0;
this.sensors.formation[1] = backVehicle ? Math.max(0, 1 - backDistance / 15) : 0;
this.sensors.formation[2] = nearbyVehicles.length / 10; // Local density
this.sensors.formation[3] = this.currentRoad ? this.currentRoad.strength : 0;
}
updateNavigationSensors() {
// Path following and navigation status
this.sensors.navigation[0] = this.currentPath.length > 0 ? 1 : 0;
this.sensors.navigation[1] = this.targetDestination ?
Math.min(1, 50 / this.mesh.position.distanceTo(this.targetDestination)) : 0;
this.sensors.navigation[2] = this.state === 'stopped' ? 1 : 0;
this.sensors.navigation[3] = this.state === 'following' ? 1 : 0;
this.sensors.navigation[4] = this.stopTimer / this.stopDuration;
this.sensors.navigation[5] = this.currentRoad ? 1 : 0;
}
update(deltaTime) {
this.updateSensors();
this.updateBehaviorState();
// Get neural network decision
const inputs = this.getAllInputs();
const outputs = this.brain.activate(inputs);
// Apply movement based on neural network and current state
this.applyMovement(outputs, deltaTime);
// Update fitness
this.updateFitness(deltaTime);
// Update visuals
this.updateVisuals();
// Update timers
this.updateTimers(deltaTime);
}
getAllInputs() {
return [
...this.sensors.obstacles, // 8 inputs
...this.sensors.roads, // 4 inputs
...this.sensors.traffic, // 6 inputs
...this.sensors.destinations, // 4 inputs
...this.sensors.formation, // 4 inputs
...this.sensors.navigation // 6 inputs
// Total: 32 inputs
];
}
updateBehaviorState() {
if (!this.targetDestination) {
this.selectNewDestination();
return;
}
const distToDestination = this.mesh.position.distanceTo(this.targetDestination);
switch (this.state) {
case 'seeking':
// Look for formation opportunities
const nearbyLeader = this.findNearbyLeader();
if (nearbyLeader && this.brain.formationPreference > 0.6) {
this.state = 'following';
this.leader = nearbyLeader;
} else if (distToDestination < 8) {
this.state = 'stopped';
this.stopTimer = this.stopDuration;
}
break;
case 'following':
if (!this.leader || this.mesh.position.distanceTo(this.leader.mesh.position) > 30) {
this.state = 'seeking';
this.leader = null;
} else if (distToDestination < 8) {
this.state = 'stopped';
this.stopTimer = this.stopDuration;
this.leader = null;
}
break;
case 'stopped':
if (this.stopTimer <= 0) {
this.selectNewDestination();
this.destinationsReached++;
}
break;
}
}
findNearbyLeader() {
const candidates = population.filter(car =>
car !== this &&
car.state !== 'stopped' &&
this.mesh.position.distanceTo(car.mesh.position) < 20 &&
this.currentRoad && car.currentRoad
);
// Find car moving in similar direction on same road
return candidates.find(car => {
const alignment = this.roadDirection.dot(car.roadDirection);
return alignment > 0.8 || car.fitness > this.fitness;
});
}
applyMovement(outputs, deltaTime) {
const [
forward, brake, turnLeft, turnRight,
emergencyStop, speedUp, formationAdjust, roadFollow,
stopHere, pathCorrect, laneChange, precision
] = outputs;
// Emergency stop override
if (emergencyStop > 0.8 || this.state === 'stopped') {
this.velocity.multiplyScalar(0.9);
return;
}
// Calculate desired direction
let desiredDirection = new THREE.Vector3(0, 0, 1);
desiredDirection.applyQuaternion(this.mesh.quaternion);
// Road following behavior
if (this.currentRoad && roadFollow > 0.3) {
const roadInfluence = roadFollow * this.brain.roadFollowingStrength;
desiredDirection.lerp(this.roadDirection, roadInfluence);
}
// Formation following
if (this.state === 'following' && this.leader) {
const followDirection = this.getFollowDirection();
const formationInfluence = formationAdjust * this.brain.formationPreference;
desiredDirection.lerp(followDirection, formationInfluence);
}
// Path following
if (this.currentPath.length > 0 && pathCorrect > 0.2) {
const pathDirection = this.getPathDirection();
if (pathDirection) {
desiredDirection.lerp(pathDirection, pathCorrect * 0.8);
}
}
// Apply turning
const currentForward = new THREE.Vector3(0, 0, 1);
currentForward.applyQuaternion(this.mesh.quaternion);
const turnAngle = currentForward.angleTo(desiredDirection);
const turnDirection = currentForward.cross(desiredDirection).y;
if (turnAngle > 0.1) {
const turnAmount = Math.sign(turnDirection) * Math.min(turnAngle, 0.05) * deltaTime;
this.mesh.rotation.y += turnAmount;
}
// Neural network turning input
const neuralTurn = (turnRight - turnLeft) * 0.03 * deltaTime;
this.mesh.rotation.y += neuralTurn;
// Speed control
let targetSpeed = this.targetSpeed;
if (this.state === 'following' && this.leader) {
const distToLeader = this.mesh.position.distanceTo(this.leader.mesh.position);
if (distToLeader < this.targetFollowingDistance) {
targetSpeed = Math.min(targetSpeed, this.leader.velocity.length() * 0.8);
}
}
if (speedUp > 0.6) targetSpeed *= 1.3;
if (brake > 0.4) targetSpeed *= 0.5;
// Apply acceleration
const forward3d = new THREE.Vector3(0, 0, 1);
forward3d.applyQuaternion(this.mesh.quaternion);
const currentSpeed = this.velocity.length();
const speedDiff = targetSpeed - currentSpeed;
if (forward > 0.2 && speedDiff > 0) {
this.acceleration.add(forward3d.multiplyScalar(speedDiff * 0.5 * deltaTime));
}
// Apply movement
this.velocity.add(this.acceleration);
this.acceleration.multiplyScalar(0.1); // Decay
// Speed limits
if (this.velocity.length() > this.maxSpeed) {
this.velocity.normalize().multiplyScalar(this.maxSpeed);
} else if (this.velocity.length() < this.minSpeed && forward > 0.1) {
this.velocity.normalize().multiplyScalar(this.minSpeed);
}
// Apply position
const oldPosition = this.mesh.position.clone();
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
this.distanceTraveled += oldPosition.distanceTo(this.mesh.position);
this.keepInBounds();
}
getFollowDirection() {
if (!this.leader) return new THREE.Vector3(0, 0, 1);
const toLeader = this.leader.mesh.position.clone().sub(this.mesh.position);
const distance = toLeader.length();
if (distance > this.targetFollowingDistance) {
return toLeader.normalize();
} else {
// Match leader's direction
return this.leader.velocity.clone().normalize();
}
}
getPathDirection() {
if (this.currentPath.length === 0 || this.pathIndex >= this.currentPath.length) {
return null;
}
const targetWaypoint = this.currentPath[this.pathIndex];
const toWaypoint = targetWaypoint.clone().sub(this.mesh.position);
const distance = toWaypoint.length();
if (distance < 15) {
this.pathIndex++;
if (this.pathIndex < this.currentPath.length) {
return this.getPathDirection();
}
}
return toWaypoint.normalize();
}
updateFitness(deltaTime) {
const oldFitness = this.fitness;
// Road usage reward
const roadBonus = this.currentRoad ? this.currentRoad.strength * deltaTime * 100 : 0;
this.roadUsageScore += roadBonus;
// Formation reward
if (this.state === 'following' && this.leader) {
const distance = this.mesh.position.distanceTo(this.leader.mesh.position);
if (distance > 5 && distance < 12) {
this.formationScore += deltaTime * 50;
}
}
// Destination reaching reward
if (this.state === 'stopped') {
this.stopComplianceScore += deltaTime * 30;
}
// Path efficiency
if (this.currentPath.length > 0) {
this.pathEfficiencyScore += deltaTime * 20;
}
// Traffic violations penalty
const nearbyVehicles = population.filter(car =>
car !== this && this.mesh.position.distanceTo(car.mesh.position) < 3
);
if (nearbyVehicles.length > 0) {
this.trafficViolations += deltaTime;
}
// Total fitness calculation
this.fitness =
this.roadUsageScore +
this.formationScore +
this.destinationsReached * 200 +
this.pathEfficiencyScore +
this.stopComplianceScore +
this.distanceTraveled * 0.5 -
this.trafficViolations * 50;
// Update global best
if (this.fitness > bestFitness) {
bestFitness = this.fitness;
}
}
updateVisuals() {
// State color coding
const stateColors = {
seeking: 0x00ff00,
following: 0x0088ff,
stopped: 0xff0000,
traveling: 0xffff00
};
this.stateIndicator.material.color.setHex(stateColors[this.state] || 0xffffff);
// Direction arrow points toward target or leader
if (this.state === 'following' && this.leader) {
const toLeader = this.leader.mesh.position.clone().sub(this.mesh.position);
this.directionArrow.lookAt(this.mesh.position.clone().add(toLeader));
} else if (this.targetDestination) {
this.directionArrow.lookAt(this.targetDestination);
}
// Body color based on performance
const performance = Math.min(this.fitness / 1000, 1);
this.bodyMaterial.color.setHSL(
performance * 0.3, // Green to red spectrum
0.8,
0.4 + performance * 0.4
);
}
updateTimers(deltaTime) {
if (this.state === 'stopped') {
this.stopTimer -= deltaTime;
}
}
getObstacles() {
const obstacles = [];
try {
if (population) {
population.forEach(car => {
if (car !== this && car.mesh) obstacles.push(car.mesh);
});
}
if (world.buildings) {
world.buildings.forEach(building => {
if (building.mesh) obstacles.push(building.mesh);
});
}
} catch (error) {
console.warn('Error getting obstacles:', error);
}
return obstacles;
}
keepInBounds() {
const bounds = 350;
if (Math.abs(this.mesh.position.x) > bounds) {
this.mesh.position.x = Math.sign(this.mesh.position.x) * bounds;
this.velocity.x *= -0.5;
}
if (Math.abs(this.mesh.position.z) > bounds) {
this.mesh.position.z = Math.sign(this.mesh.position.z) * bounds;
this.velocity.z *= -0.5;
}
}
destroy() {
try {
if (this.pathLine && this.pathLine.parent) {
scene.remove(this.pathLine);
if (this.pathLine.geometry) this.pathLine.geometry.dispose();
if (this.pathLine.material) this.pathLine.material.dispose();
}
if (this.destinationMarker && this.destinationMarker.parent) {
scene.remove(this.destinationMarker);
if (this.destinationMarker.geometry) this.destinationMarker.geometry.dispose();
if (this.destinationMarker.material) this.destinationMarker.material.dispose();
}
if (this.mesh && this.mesh.parent) {
scene.remove(this.mesh);
// Dispose of car mesh materials and geometries
this.mesh.traverse((child) => {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(mat => mat.dispose());
} else {
child.material.dispose();
}
}
});
}
} catch (error) {
console.warn('Cleanup error:', error);
}
}
}
function init() {
// Scene setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 200, 800);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 80, 80);
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setClearColor(0x001122);
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 100, 50);
directionalLight.castShadow = true;
scene.add(directionalLight);
createWorld();
createPopulation();
clock = new THREE.Clock();
window.addEventListener('resize', onWindowResize);
setupControls();
animate();
}
function createWorld() {
// Ground
const groundGeometry = new THREE.PlaneGeometry(800, 800);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
createRoadNetwork();
createBuildings();
}
function createRoadNetwork() {
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
// Create grid of roads
for (let x = -300; x <= 300; x += ROAD_SPACING) {
// Vertical road
const vRoadGeometry = new THREE.PlaneGeometry(ROAD_WIDTH, 600);
const vRoad = new THREE.Mesh(vRoadGeometry, roadMaterial);
vRoad.rotation.x = -Math.PI / 2;
vRoad.position.set(x, 0.1, 0);
scene.add(vRoad);
// Lane dividers
for (let z = -290; z <= 290; z += 20) {
const dividerGeometry = new THREE.PlaneGeometry(0.5, 8);
const dividerMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
const divider = new THREE.Mesh(dividerGeometry, dividerMaterial);
divider.rotation.x = -Math.PI / 2;
divider.position.set(x, 0.2, z);
scene.add(divider);
}
}
for (let z = -300; z <= 300; z += ROAD_SPACING) {
// Horizontal road
const hRoadGeometry = new THREE.PlaneGeometry(600, ROAD_WIDTH);
const hRoad = new THREE.Mesh(hRoadGeometry, roadMaterial);
hRoad.rotation.x = -Math.PI / 2;
hRoad.position.set(0, 0.1, z);
scene.add(hRoad);
// Lane dividers
for (let x = -290; x <= 290; x += 20) {
const dividerGeometry = new THREE.PlaneGeometry(8, 0.5);
const dividerMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
const divider = new THREE.Mesh(dividerGeometry, dividerMaterial);
divider.rotation.x = -Math.PI / 2;
divider.position.set(x, 0.2, z);
scene.add(divider);
}
}
}
function createBuildings() {
world.buildings = [];
const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 });
// Place buildings at strategic locations away from roads
const buildingPositions = [
[-180, -180], [-60, -180], [60, -180], [180, -180],
[-180, -60], [-60, -60], [60, -60], [180, -60],
[-180, 60], [-60, 60], [60, 60], [180, 60],
[-180, 180], [-60, 180], [60, 180], [180, 180]
];
buildingPositions.forEach(([x, z]) => {
const width = 20 + Math.random() * 15;
const height = 10 + Math.random() * 25;
const depth = 20 + Math.random() * 15;
const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
building.position.set(x, height / 2, z);
building.castShadow = true;
scene.add(building);
world.buildings.push({
mesh: building,
position: new THREE.Vector3(x, 0, z)
});
});
}
function createPopulation() {
population = [];
// Start cars at various intersections
const startPositions = [
[-120, -120], [0, -120], [120, -120],
[-120, 0], [0, 0], [120, 0],
[-120, 120], [0, 120], [120, 120]
];
for (let i = 0; i < populationSize; i++) {
const startPos = startPositions[i % startPositions.length];
const x = startPos[0] + (Math.random() - 0.5) * 20;
const z = startPos[1] + (Math.random() - 0.5) * 20;
const car = new RoadNavigationCar(x, z);
population.push(car);
scene.add(car.mesh);
}
}
function evolve() {
console.log(`Epoch ${epoch} complete. Best fitness: ${bestFitness.toFixed(1)}`);
// Sort population by fitness
population.sort((a, b) => b.fitness - a.fitness);
// Keep top performers
const eliteCount = Math.floor(populationSize * 0.3);
const newPopulation = [];
// Elite selection
for (let i = 0; i < eliteCount; i++) {
const elite = population[i];
const newCar = createNewCar();
newCar.brain = elite.brain.copy();
newPopulation.push(newCar);
}
// Crossover and mutation
while (newPopulation.length < populationSize) {
const parent1 = tournamentSelection();
const parent2 = tournamentSelection();
const child = createNewCar();
child.brain = parent1.brain.crossover(parent2.brain);
child.brain.mutate(0.1);
newPopulation.push(child);
}
// Replace population
population.forEach(car => car.destroy());
population = newPopulation;
population.forEach(car => scene.add(car.mesh));
// Reset for new epoch
epoch++;
timeLeft = epochTime;
}
function createNewCar() {
const startPositions = [
[-120, -120], [0, -120], [120, -120],
[-120, 0], [0, 0], [120, 0],
[-120, 120], [0, 120], [120, 120]
];
const startPos = startPositions[Math.floor(Math.random() * startPositions.length)];
const x = startPos[0] + (Math.random() - 0.5) * 20;
const z = startPos[1] + (Math.random() - 0.5) * 20;
return new RoadNavigationCar(x, z);
}
function tournamentSelection(tournamentSize = 3) {
let best = null;
let bestFitness = -Infinity;
for (let i = 0; i < tournamentSize; i++) {
const candidate = population[Math.floor(Math.random() * population.length)];
if (candidate.fitness > bestFitness) {
best = candidate;
bestFitness = candidate.fitness;
}
}
return best;
}
function animate() {
requestAnimationFrame(animate);
if (!paused) {
const deltaTime = Math.min(clock.getDelta() * speedMultiplier, 0.05);
timeLeft -= deltaTime;
if (timeLeft <= 0) {
evolve();
}
updatePopulation(deltaTime);
updateCamera();
updateUI();
}
renderer.render(scene, camera);
}
function updatePopulation(deltaTime) {
population.forEach(car => car.update(deltaTime));
}
function updateCamera() {
if (cameraMode === 'follow') {
const bestCar = population.reduce((best, car) =>
car.fitness > best.fitness ? car : best
);
const targetPos = bestCar.mesh.position.clone();
targetPos.y += 40;
targetPos.add(bestCar.velocity.clone().normalize().multiplyScalar(20));
camera.position.lerp(targetPos, 0.02);
camera.lookAt(bestCar.mesh.position);
} else {
camera.position.lerp(new THREE.Vector3(0, 150, 150), 0.02);
camera.lookAt(0, 0, 0);
}
}
function updateUI() {
// Basic stats
document.getElementById('epoch').textContent = epoch;
document.getElementById('epochTime').textContent = Math.ceil(timeLeft);
document.getElementById('population').textContent = population.length;
document.getElementById('bestFitness').textContent = Math.round(bestFitness);
// Progress bar
const progress = ((epochTime - timeLeft) / epochTime) * 100;
document.getElementById('timeProgress').style.width = `${progress}%`;
// Calculate aggregate stats
let stats = {
onRoad: 0,
traveling: 0,
stopped: 0,
navigating: 0,
totalDestinations: 0,
totalRoadUsage: 0,
totalFormation: 0,
totalPathEfficiency: 0,
totalStopCompliance: 0,
totalViolations: 0,
singleFileLines: 0,
totalLineLength: 0
};
population.forEach(car => {
if (car.currentRoad) stats.onRoad++;
if (car.state === 'seeking') stats.traveling++;
if (car.state === 'stopped') stats.stopped++;
if (car.currentPath.length > 0) stats.navigating++;
stats.totalDestinations += car.destinationsReached;
stats.totalRoadUsage += car.roadUsageScore;
stats.totalFormation += car.formationScore;
stats.totalPathEfficiency += car.pathEfficiencyScore;
stats.totalStopCompliance += car.stopComplianceScore;
stats.totalViolations += car.trafficViolations;
});
// Update UI elements
document.getElementById('onRoadCount').textContent = stats.onRoad;
document.getElementById('travelingCount').textContent = stats.traveling;
document.getElementById('stoppedCount').textContent = stats.stopped;
document.getElementById('navigatingCount').textContent = stats.navigating;
document.getElementById('destinationsReached').textContent = stats.totalDestinations;
document.getElementById('roadUsage').textContent = Math.round((stats.totalRoadUsage / population.length) / 10);
document.getElementById('formationQuality').textContent = Math.round((stats.totalFormation / population.length) / 10);
document.getElementById('pathEfficiency').textContent = Math.round((stats.totalPathEfficiency / population.length) / 10);
document.getElementById('stopCompliance').textContent = Math.round((stats.totalStopCompliance / population.length) / 10);
document.getElementById('trafficViolations').textContent = Math.round(stats.totalViolations);
// Behavioral stats
const avgRoadFollowing = population.reduce((sum, car) => sum + car.brain.roadFollowingStrength, 0) / population.length;
const avgFormationPref = population.reduce((sum, car) => sum + car.brain.formationPreference, 0) / population.length;
const avgNavSkill = population.reduce((sum, car) => sum + car.brain.navigationSkill, 0) / population.length;
document.getElementById('roadFollowing').textContent = Math.round(avgRoadFollowing * 100);
document.getElementById('formationControl').textContent = Math.round(avgFormationPref * 100);
document.getElementById('navigationSkills').textContent = Math.round(avgNavSkill * 100);
document.getElementById('navigationIQ').textContent = Math.round(avgNavSkill * 100);
document.getElementById('roadMastery').textContent = Math.round(avgRoadFollowing * 100);
}
function setupControls() {
document.getElementById('pauseBtn').addEventListener('click', () => {
paused = !paused;
document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause';
});
document.getElementById('resetBtn').addEventListener('click', () => {
population.forEach(car => car.destroy());
epoch = 1;
timeLeft = epochTime;
bestFitness = 0;
createPopulation();
});
document.getElementById('speedBtn').addEventListener('click', () => {
speedMultiplier = speedMultiplier === 1 ? 2 : speedMultiplier === 2 ? 5 : 1;
document.getElementById('speedBtn').textContent = `Speed: ${speedMultiplier}x`;
});
document.getElementById('viewBtn').addEventListener('click', () => {
cameraMode = cameraMode === 'follow' ? 'overview' : 'follow';
document.getElementById('viewBtn').textContent = `View: ${cameraMode === 'follow' ? 'Follow' : 'Overview'}`;
});
document.getElementById('pathBtn').addEventListener('click', () => {
showPaths = !showPaths;
document.getElementById('pathBtn').textContent = `Paths: ${showPaths ? 'ON' : 'OFF'}`;
population.forEach(car => {
if (car.pathLine) {
car.pathLine.visible = showPaths;
if (showPaths) {
car.updatePathVisualization();
}
}
});
});
document.getElementById('debugBtn').addEventListener('click', () => {
debugMode = !debugMode;
document.getElementById('debugBtn').textContent = `Debug: ${debugMode ? 'ON' : 'OFF'}`;
});
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
init();
</script>
</body>
</html>