ParkingLotDrivingSim / index.html
awacke1's picture
Update index.html
edac4ed verified
raw
history blame
73.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enhanced AI Traffic 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;
}
#flockingStats {
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;
}
#trafficStats {
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; }
.flocking { color: #00aaff; }
.solo { color: #ff8800; }
.leader { color: #ff00ff; font-weight: bold; }
.convoy { color: #00ffff; }
.parked { color: #88ff88; }
.species-0 { color: #ff6b6b; }
.species-1 { color: #4ecdc4; }
.species-2 { color: #45b7d1; }
.species-3 { color: #96ceb4; }
.species-4 { color: #ffd93d; }
.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">AI Traffic Evolution Simulator</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>Species: <span id="speciesCount">1</span></div>
<div>Best Fitness: <span id="bestFitness">0</span></div>
<div>Traffic IQ: <span id="trafficIQ">50</span></div>
<div>Road Mastery: <span id="roadMastery">0</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: Overview</button>
<button id="flockBtn">Networks: ON</button>
<button id="trafficBtn">Traffic Rules: ON</button>
</div>
<div id="stats">
<div><span class="highlight">Top Performers:</span></div>
<div id="topPerformers"></div>
<div style="margin-top: 10px;"><span class="highlight">Generation Stats:</span></div>
<div>Crashes: <span id="crashCount">0</span></div>
<div>Total Distance: <span id="totalDistance">0</span></div>
<div>Parking Events: <span id="parkingEvents">0</span></div>
<div>Lane Violations: <span id="laneViolations">0</span></div>
<div>Convoy Length: <span id="convoyLength">0</span></div>
</div>
<div id="flockingStats">
<div><span class="highlight">Convoy Behavior:</span></div>
<div><span class="leader">Leaders:</span> <span id="leaderCount">0</span></div>
<div><span class="convoy">In Convoy:</span> <span id="convoyCount">0</span></div>
<div><span class="parked">Parked:</span> <span id="parkedCount">0</span></div>
<div><span class="solo">Solo:</span> <span id="soloCount">0</span></div>
<div>Largest Convoy: <span id="largestConvoy">0</span></div>
<div>Formation Quality: <span id="formationQuality">0</span>%</div>
<div>Parking Efficiency: <span id="parkingEfficiency">0</span>%</div>
</div>
<div id="trafficStats">
<div><span class="highlight">Traffic Intelligence:</span></div>
<div>Lane Discipline: <span id="laneDiscipline">0</span>%</div>
<div>Following Distance: <span id="followingDistance">0</span>m</div>
<div>Road Adherence: <span id="roadAdherence">0</span>%</div>
<div>Turn Signals: <span id="turnSignals">0</span>%</div>
<div style="margin-top: 10px;"><span class="highlight">Parking:</span></div>
<div>Spots Occupied: <span id="spotsOccupied">0</span></div>
<div>Parking Success: <span id="parkingSuccess">0</span>%</div>
<div>Queue Efficiency: <span id="queueEfficiency">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: [],
parkingLots: [],
flockLines: []
};
// Enhanced evolution system
let epoch = 1;
let epochTime = 60;
let timeLeft = 60;
let population = [];
let species = [];
let populationSize = 100;
let bestFitness = 0;
let crashCount = 0;
let paused = false;
let speedMultiplier = 1;
let cameraMode = 'overview'; // 'overview', 'follow_best', 'follow_convoy'
let showFlockLines = true;
let trafficRules = true;
let parkingEvents = 0;
let laneViolations = 0;
// Traffic and road parameters
const ROAD_WIDTH = 12;
const LANE_WIDTH = 6;
const ROAD_SPACING = 150;
const FOLLOW_DISTANCE = 8;
const CONVOY_MAX_DISTANCE = 12;
const PARKING_SPOT_SIZE = { width: 4, length: 8 };
// Enhanced Neural Network for traffic behavior
class TrafficAI {
constructor() {
this.inputSize = 28; // Enhanced traffic-aware inputs
this.hiddenLayers = [36, 28, 20];
this.outputSize = 10; // More nuanced traffic outputs
this.memorySize = 8;
this.weights = [];
this.biases = [];
this.memory = new Array(this.memorySize).fill(0);
this.memoryPointer = 0;
// Build network
let prevSize = this.inputSize + this.memorySize;
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));
// Traffic-specific traits
this.trafficTraits = {
laneKeeping: Math.random(),
followingBehavior: Math.random(),
parkingSkill: Math.random(),
convoyDiscipline: Math.random(),
roadPriority: Math.random()
};
}
randomMatrix(rows, cols) {
let matrix = [];
for (let i = 0; i < rows; i++) {
matrix[i] = [];
for (let j = 0; j < cols; j++) {
matrix[i][j] = (Math.random() - 0.5) * 2;
}
}
return matrix;
}
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]);
this.updateMemory(inputs, outputs);
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.sigmoid(outputs[i]);
}
return outputs;
}
sigmoid(x) {
return 1 / (1 + Math.exp(-Math.max(-10, Math.min(10, x))));
}
updateMemory(inputs, outputs) {
const roadInfo = inputs.slice(20, 24).reduce((a, b) => a + b, 0) / 4;
this.memory[this.memoryPointer] = roadInfo;
this.memoryPointer = (this.memoryPointer + 1) % this.memorySize;
}
mutate(rate = 0.1) {
this.weights.forEach(weightMatrix => {
this.mutateMatrix(weightMatrix, rate);
});
this.biases.forEach(biasArray => {
this.mutateArray(biasArray, rate);
});
Object.keys(this.trafficTraits).forEach(trait => {
if (Math.random() < rate) {
this.trafficTraits[trait] += (Math.random() - 0.5) * 0.2;
this.trafficTraits[trait] = Math.max(0, Math.min(1, this.trafficTraits[trait]));
}
});
}
mutateMatrix(matrix, rate) {
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]));
}
}
}
}
mutateArray(array, rate) {
for (let i = 0; i < array.length; i++) {
if (Math.random() < rate) {
array[i] += (Math.random() - 0.5) * 0.5;
array[i] = Math.max(-3, Math.min(3, array[i]));
}
}
}
copy() {
const newAI = new TrafficAI();
newAI.weights = this.weights.map(matrix => matrix.map(row => [...row]));
newAI.biases = this.biases.map(bias => [...bias]);
newAI.memory = [...this.memory];
newAI.memoryPointer = this.memoryPointer;
newAI.trafficTraits = {...this.trafficTraits};
return newAI;
}
}
// Enhanced AI Car with traffic behavior
class TrafficCar {
constructor(x = 0, z = 0) {
this.brain = new TrafficAI();
this.mesh = this.createCarMesh();
this.mesh.position.set(x, 1, z);
// Movement and traffic properties
this.velocity = new THREE.Vector3();
this.acceleration = new THREE.Vector3();
this.maxSpeed = 20;
this.minSpeed = 2;
this.currentLane = null;
this.targetLane = null;
this.lanePosition = 0; // -1 to 1 within lane
// Convoy and flock behavior
this.flockId = -1;
this.convoyPosition = -1; // Position in convoy (-1 = not in convoy)
this.convoyLeader = null;
this.convoyFollowers = [];
this.followTarget = null;
this.role = 'driver'; // driver, leader, parker
// Traffic behavior
this.isParked = false;
this.parkingSpot = null;
this.targetParkingLot = null;
this.turnSignal = 'none'; // left, right, none
this.laneDiscipline = 0;
this.followingDistance = FOLLOW_DISTANCE;
// Fitness and metrics
this.fitness = 0;
this.roadTime = 0;
this.convoyTime = 0;
this.parkingScore = 0;
this.trafficViolations = 0;
this.distanceTraveled = 0;
this.crashed = false;
this.timeAlive = 100;
// Sensors and visualization
this.sensors = Array(16).fill(0);
this.roadSensors = Array(8).fill(0);
this.trafficSensors = Array(4).fill(0);
this.sensorRays = [];
this.flockLines = [];
this.neighbors = [];
this.lastPosition = new THREE.Vector3(x, 1, z);
this.createSensorRays();
this.createFlockVisualization();
this.initializeMovement();
}
createCarMesh() {
const group = new THREE.Group();
// Car body
const bodyGeometry = new THREE.BoxGeometry(1.5, 0.8, 3.5);
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);
// Turn signals
const signalGeometry = new THREE.SphereGeometry(0.15, 6, 4);
this.leftSignal = new THREE.Mesh(signalGeometry,
new THREE.MeshLambertMaterial({ color: 0xff8800, transparent: true, opacity: 0.5 }));
this.leftSignal.position.set(-0.8, 0.8, 1.2);
group.add(this.leftSignal);
this.rightSignal = new THREE.Mesh(signalGeometry,
new THREE.MeshLambertMaterial({ color: 0xff8800, transparent: true, opacity: 0.5 }));
this.rightSignal.position.set(0.8, 0.8, 1.2);
group.add(this.rightSignal);
// Role indicator
const indicatorGeometry = new THREE.ConeGeometry(0.2, 0.8, 6);
this.roleIndicator = new THREE.Mesh(indicatorGeometry,
new THREE.MeshLambertMaterial({ color: 0xffffff }));
this.roleIndicator.position.set(0, 1.5, 0);
group.add(this.roleIndicator);
// Wheels with proper rotation
const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 8);
const wheelMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
this.wheels = [];
const wheelPositions = [
[-0.7, 0, 1.4], [0.7, 0, 1.4],
[-0.7, 0, -1.4], [0.7, 0, -1.4]
];
wheelPositions.forEach((pos, i) => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheel.position.set(...pos);
wheel.rotation.z = Math.PI / 2;
this.wheels.push(wheel);
group.add(wheel);
});
return group;
}
createSensorRays() {
const sensorMaterial = new THREE.LineBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.2
});
for (let i = 0; i < 16; i++) {
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, 10)
]);
const ray = new THREE.Line(geometry, sensorMaterial);
this.sensorRays.push(ray);
this.mesh.add(ray);
}
}
createFlockVisualization() {
const flockMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.6,
linewidth: 2
});
for (let i = 0; i < 10; i++) {
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 2, 0),
new THREE.Vector3(0, 2, 0)
]);
const line = new THREE.Line(geometry, flockMaterial);
this.flockLines.push(line);
if (showFlockLines) scene.add(line);
}
}
initializeMovement() {
// Start on a road if possible
const nearestRoad = this.findNearestRoad();
if (nearestRoad) {
this.currentLane = nearestRoad.lane;
this.mesh.rotation.y = nearestRoad.direction;
this.velocity.set(
Math.sin(nearestRoad.direction) * 8,
0,
Math.cos(nearestRoad.direction) * 8
);
} else {
this.mesh.rotation.y = Math.random() * Math.PI * 2;
this.velocity.set(
Math.sin(this.mesh.rotation.y) * 6,
0,
Math.cos(this.mesh.rotation.y) * 6
);
}
}
findNearestRoad() {
const pos = this.mesh.position;
let nearestRoad = null;
let minDistance = Infinity;
// Check horizontal roads
for (let roadZ = -300; roadZ <= 300; roadZ += ROAD_SPACING) {
const distToRoad = Math.abs(pos.z - roadZ);
if (distToRoad < ROAD_WIDTH / 2 && distToRoad < minDistance) {
minDistance = distToRoad;
const laneCenter = roadZ + (pos.x > 0 ? -LANE_WIDTH/2 : LANE_WIDTH/2);
nearestRoad = {
lane: 'horizontal',
center: laneCenter,
direction: pos.x > 0 ? Math.PI : 0 // Left side driving
};
}
}
// Check vertical roads
for (let roadX = -300; roadX <= 300; roadX += ROAD_SPACING) {
const distToRoad = Math.abs(pos.x - roadX);
if (distToRoad < ROAD_WIDTH / 2 && distToRoad < minDistance) {
minDistance = distToRoad;
const laneCenter = roadX + (pos.z > 0 ? -LANE_WIDTH/2 : LANE_WIDTH/2);
nearestRoad = {
lane: 'vertical',
center: laneCenter,
direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2 // Left side driving
};
}
}
return nearestRoad;
}
updateSensors() {
const maxDistance = 10;
const raycaster = new THREE.Raycaster();
// 16-direction obstacle sensors
for (let i = 0; i < 16; i++) {
const angle = (i * Math.PI * 2) / 16;
const direction = new THREE.Vector3(
Math.sin(angle), 0, Math.cos(angle)
);
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 <= maxDistance) {
this.sensors[i] = 1 - (intersects[0].distance / maxDistance);
} else {
this.sensors[i] = 0;
}
// Update visual rays
const endDistance = intersects.length > 0 ?
Math.min(intersects[0].distance, maxDistance) : maxDistance;
const rayEnd = direction.clone().multiplyScalar(endDistance);
this.sensorRays[i].geometry.setFromPoints([
new THREE.Vector3(0, 0, 0), rayEnd
]);
}
// Road-specific sensors
this.updateRoadSensors();
this.updateTrafficSensors();
}
updateRoadSensors() {
const pos = this.mesh.position;
// Road position and lane detection
this.roadSensors[0] = this.getRoadPosition();
this.roadSensors[1] = this.getLanePosition();
this.roadSensors[2] = this.getRoadDirection();
this.roadSensors[3] = this.getDistanceToIntersection();
// Parking lot detection
this.roadSensors[4] = this.getNearestParkingLot();
this.roadSensors[5] = this.getParkingAvailability();
// Traffic flow
this.roadSensors[6] = this.getTrafficDensity();
this.roadSensors[7] = this.getOptimalSpeed();
}
updateTrafficSensors() {
// Convoy and following behavior
this.trafficSensors[0] = this.getConvoyStatus();
this.trafficSensors[1] = this.getFollowingDistance();
this.trafficSensors[2] = this.getLeaderDistance();
this.trafficSensors[3] = this.getNeedToPark();
}
getRoadPosition() {
const pos = this.mesh.position;
// Check horizontal roads
for (let roadZ = -300; roadZ <= 300; roadZ += ROAD_SPACING) {
const distToRoad = Math.abs(pos.z - roadZ);
if (distToRoad <= ROAD_WIDTH / 2) {
return 1 - (distToRoad / (ROAD_WIDTH / 2));
}
}
// Check vertical roads
for (let roadX = -300; roadX <= 300; roadX += ROAD_SPACING) {
const distToRoad = Math.abs(pos.x - roadX);
if (distToRoad <= ROAD_WIDTH / 2) {
return 1 - (distToRoad / (ROAD_WIDTH / 2));
}
}
return 0; // Off road
}
getLanePosition() {
const pos = this.mesh.position;
const roadInfo = this.findNearestRoad();
if (!roadInfo) return 0.5;
if (roadInfo.lane === 'horizontal') {
const laneOffset = pos.z - roadInfo.center;
return 0.5 + (laneOffset / LANE_WIDTH);
} else {
const laneOffset = pos.x - roadInfo.center;
return 0.5 + (laneOffset / LANE_WIDTH);
}
}
getRoadDirection() {
const roadInfo = this.findNearestRoad();
if (!roadInfo) return 0.5;
const currentDirection = Math.atan2(this.velocity.x, this.velocity.z);
const targetDirection = roadInfo.direction;
let angleDiff = targetDirection - currentDirection;
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
return 0.5 + (angleDiff / Math.PI) * 0.5;
}
getDistanceToIntersection() {
const pos = this.mesh.position;
let minDist = Infinity;
// Find distance to nearest intersection
for (let x = -300; x <= 300; x += ROAD_SPACING) {
for (let z = -300; z <= 300; z += ROAD_SPACING) {
const dist = pos.distanceTo(new THREE.Vector3(x, 0, z));
minDist = Math.min(minDist, dist);
}
}
return Math.max(0, 1 - minDist / 50);
}
getNearestParkingLot() {
const pos = this.mesh.position;
let nearestDist = Infinity;
world.parkingLots.forEach(lot => {
const dist = pos.distanceTo(lot.center);
if (dist < nearestDist) {
nearestDist = dist;
this.targetParkingLot = lot;
}
});
return Math.max(0, 1 - nearestDist / 100);
}
getParkingAvailability() {
if (!this.targetParkingLot) return 0;
const availableSpots = this.targetParkingLot.spots.filter(spot => !spot.occupied).length;
return availableSpots / this.targetParkingLot.spots.length;
}
getTrafficDensity() {
const pos = this.mesh.position;
let nearbyCount = 0;
population.forEach(other => {
if (other !== this && !other.crashed && !other.isParked) {
const dist = pos.distanceTo(other.mesh.position);
if (dist < 30) nearbyCount++;
}
});
return Math.min(nearbyCount / 10, 1);
}
getOptimalSpeed() {
const roadPos = this.getRoadPosition();
const density = this.getTrafficDensity();
return roadPos * (1 - density * 0.5);
}
getConvoyStatus() {
return this.convoyPosition >= 0 ? 1 : 0;
}
getFollowingDistance() {
if (!this.followTarget) return 1;
const dist = this.mesh.position.distanceTo(this.followTarget.mesh.position);
return Math.min(dist / 20, 1);
}
getLeaderDistance() {
if (!this.convoyLeader) return 0;
const dist = this.mesh.position.distanceTo(this.convoyLeader.mesh.position);
return Math.max(0, 1 - dist / 50);
}
getNeedToPark() {
return (this.timeAlive < 30 && !this.isParked) ? 1 : 0;
}
updateConvoyBehavior() {
this.neighbors = [];
// Find nearby cars for convoy formation
population.forEach(other => {
if (other !== this && !other.crashed && !other.isParked) {
const distance = this.mesh.position.distanceTo(other.mesh.position);
if (distance < 25) {
this.neighbors.push(other);
}
}
});
// Determine role and convoy behavior
this.updateRole();
this.updateConvoyFormation();
}
updateRole() {
const roadPos = this.getRoadPosition();
if (roadPos > 0.8 && this.neighbors.length > 2 && this.brain.trafficTraits.convoyDiscipline > 0.7) {
this.role = 'leader';
this.roleIndicator.material.color.setHex(0xff00ff);
} else if (this.getNeedToPark() > 0.5) {
this.role = 'parker';
this.roleIndicator.material.color.setHex(0x00ff00);
} else {
this.role = 'driver';
this.roleIndicator.material.color.setHex(0xffffff);
}
}
updateConvoyFormation() {
if (this.role === 'leader') {
// Leaders organize convoys
this.convoyFollowers = this.neighbors
.filter(car => car.role === 'driver')
.sort((a, b) =>
this.mesh.position.distanceTo(a.mesh.position) -
this.mesh.position.distanceTo(b.mesh.position)
)
.slice(0, 5); // Max 5 followers
this.convoyFollowers.forEach((follower, index) => {
follower.convoyLeader = this;
follower.convoyPosition = index;
follower.followTarget = index === 0 ? this : this.convoyFollowers[index - 1];
});
} else if (this.convoyPosition >= 0) {
// Update following behavior
if (this.followTarget && this.followTarget.crashed) {
this.convoyPosition = -1;
this.convoyLeader = null;
this.followTarget = null;
}
}
}
getEnhancedInputs() {
return [
...this.sensors, // 16 obstacle sensors
...this.roadSensors, // 8 road/navigation sensors
...this.trafficSensors, // 4 traffic behavior sensors
];
}
update(deltaTime) {
if (this.crashed || this.isParked) return;
this.timeAlive -= deltaTime;
if (this.timeAlive <= 0) {
this.attemptParking();
return;
}
this.updateSensors();
this.updateConvoyBehavior();
this.updateVisuals();
// Get AI decision
const inputs = this.getEnhancedInputs();
const outputs = this.brain.activate(inputs);
// Apply traffic-aware movement
this.applyTrafficMovement(outputs, deltaTime);
this.updateFitness(deltaTime);
this.lastPosition.copy(this.mesh.position);
this.checkCollisions();
this.keepInBounds();
}
applyTrafficMovement(outputs, deltaTime) {
const [
acceleration, braking, steerLeft, steerRight,
laneChange, followConvoy, parkingManeuver, turnSignalLeft,
turnSignalRight, emergencyStop
] = outputs;
// Update turn signals
this.turnSignal = 'none';
if (turnSignalLeft > 0.7) this.turnSignal = 'left';
if (turnSignalRight > 0.7) this.turnSignal = 'right';
this.leftSignal.material.opacity = this.turnSignal === 'left' ? 1.0 : 0.3;
this.rightSignal.material.opacity = this.turnSignal === 'right' ? 1.0 : 0.3;
// Emergency stop
if (emergencyStop > 0.8) {
this.velocity.multiplyScalar(0.7);
return;
}
// Parking maneuver
if (parkingManeuver > 0.8 && this.targetParkingLot) {
this.executeParking(deltaTime);
return;
}
// Road-following behavior
this.followRoad(deltaTime);
// Convoy following
if (followConvoy > 0.6 && this.followTarget) {
this.followConvoyTarget(deltaTime);
}
// Basic movement
const forward = new THREE.Vector3(0, 0, 1);
forward.applyQuaternion(this.mesh.quaternion);
// Acceleration and braking
if (acceleration > 0.3) {
this.velocity.add(forward.multiplyScalar(acceleration * 8 * deltaTime));
}
if (braking > 0.5) {
this.velocity.multiplyScalar(1 - braking * deltaTime * 3);
}
// Steering
const steering = (steerRight - steerLeft) * 0.08 * deltaTime;
this.mesh.rotation.y += steering;
// Speed limits
const currentSpeed = this.velocity.length();
if (currentSpeed > this.maxSpeed) {
this.velocity.normalize().multiplyScalar(this.maxSpeed);
} else if (currentSpeed < this.minSpeed && currentSpeed > 0.1) {
this.velocity.normalize().multiplyScalar(this.minSpeed);
}
// Apply movement
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
// Wheel rotation
this.wheels.forEach(wheel => {
wheel.rotation.x += currentSpeed * deltaTime * 0.1;
});
}
followRoad(deltaTime) {
const roadInfo = this.findNearestRoad();
if (!roadInfo) return;
// Lane-keeping force
const laneKeepingForce = this.brain.trafficTraits.laneKeeping;
let targetDirection = roadInfo.direction;
// Adjust for lane position
if (roadInfo.lane === 'horizontal') {
const laneOffset = this.mesh.position.z - roadInfo.center;
if (Math.abs(laneOffset) > LANE_WIDTH / 4) {
const correction = -laneOffset * 0.02 * laneKeepingForce;
this.velocity.z += correction;
if (Math.abs(laneOffset) > LANE_WIDTH / 2) {
this.trafficViolations++;
laneViolations++;
}
}
} else {
const laneOffset = this.mesh.position.x - roadInfo.center;
if (Math.abs(laneOffset) > LANE_WIDTH / 4) {
const correction = -laneOffset * 0.02 * laneKeepingForce;
this.velocity.x += correction;
if (Math.abs(laneOffset) > LANE_WIDTH / 2) {
this.trafficViolations++;
laneViolations++;
}
}
}
// Direction alignment
const currentDirection = Math.atan2(this.velocity.x, this.velocity.z);
let angleDiff = targetDirection - currentDirection;
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
if (Math.abs(angleDiff) > 0.1) {
this.mesh.rotation.y += angleDiff * 0.05 * laneKeepingForce;
}
this.roadTime += deltaTime;
this.laneDiscipline = Math.max(0, 1 - this.trafficViolations * 0.1);
}
followConvoyTarget(deltaTime) {
if (!this.followTarget) return;
const targetPos = this.followTarget.mesh.position;
const distance = this.mesh.position.distanceTo(targetPos);
const idealDistance = FOLLOW_DISTANCE + (this.convoyPosition * 2);
if (distance > idealDistance + 3) {
// Too far - speed up
const catchUpForce = this.followTarget.velocity.clone().multiplyScalar(0.3);
this.velocity.add(catchUpForce.multiplyScalar(deltaTime));
} else if (distance < idealDistance - 2) {
// Too close - slow down
this.velocity.multiplyScalar(0.95);
}
// Align with target's direction
const targetDirection = Math.atan2(this.followTarget.velocity.x, this.followTarget.velocity.z);
const currentDirection = Math.atan2(this.velocity.x, this.velocity.z);
let angleDiff = targetDirection - currentDirection;
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
this.mesh.rotation.y += angleDiff * 0.1;
this.convoyTime += deltaTime;
this.followingDistance = distance;
}
executeParking(deltaTime) {
if (!this.targetParkingLot) return;
// Find available parking spot
const availableSpot = this.targetParkingLot.spots.find(spot => !spot.occupied);
if (!availableSpot) return;
const spotPos = availableSpot.position;
const distance = this.mesh.position.distanceTo(spotPos);
if (distance < 2) {
// Park successfully
this.isParked = true;
this.parkingSpot = availableSpot;
availableSpot.occupied = true;
availableSpot.car = this;
this.mesh.position.copy(spotPos);
this.velocity.set(0, 0, 0);
this.parkingScore += 100;
parkingEvents++;
this.updateCarColor();
} else {
// Move toward parking spot
const direction = spotPos.clone().sub(this.mesh.position).normalize();
this.velocity.copy(direction.multiplyScalar(5));
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
// Align with parking spot
const targetAngle = Math.atan2(direction.x, direction.z);
this.mesh.rotation.y = targetAngle;
}
}
attemptParking() {
if (!this.targetParkingLot) {
this.crashed = true;
return;
}
this.executeParking(0.016); // Try one more time
if (!this.isParked) {
this.crashed = true;
}
}
updateFitness(deltaTime) {
const distance = this.mesh.position.distanceTo(this.lastPosition);
this.distanceTraveled += distance;
// Multi-objective fitness
const roadBonus = this.getRoadPosition() * distance * 5;
const laneBonus = this.laneDiscipline * distance * 3;
const convoyBonus = this.convoyTime * 2;
const parkingBonus = this.parkingScore;
const violationPenalty = this.trafficViolations * -10;
this.fitness = this.distanceTraveled +
roadBonus +
laneBonus +
convoyBonus +
parkingBonus +
violationPenalty;
}
updateVisuals() {
this.updateCarColor();
this.updateFlockVisualization();
}
updateCarColor() {
let hue = 0.6; // Default blue
let saturation = 0.7;
let lightness = 0.5;
if (this.isParked) {
hue = 0.3; // Green for parked
lightness = 0.7;
} else if (this.role === 'leader') {
hue = 0.8; // Purple for leaders
saturation = 1.0;
lightness = 0.6;
} else if (this.convoyPosition >= 0) {
hue = 0.5; // Cyan for convoy members
saturation = 0.8;
lightness = 0.6;
} else if (this.getRoadPosition() < 0.3) {
hue = 0.1; // Orange for off-road
saturation = 1.0;
lightness = 0.5;
}
// Performance-based brightness
const performanceBonus = Math.min(this.fitness / 500, 0.2);
lightness += performanceBonus;
this.bodyMaterial.color.setHSL(hue, saturation, lightness);
}
updateFlockVisualization() {
if (!showFlockLines) return;
// Show convoy connections
let connectionIndex = 0;
// Leader to followers
if (this.role === 'leader') {
this.convoyFollowers.forEach(follower => {
if (connectionIndex < this.flockLines.length) {
const start = this.mesh.position.clone();
start.y = 3;
const end = follower.mesh.position.clone();
end.y = 3;
this.flockLines[connectionIndex].geometry.setFromPoints([start, end]);
this.flockLines[connectionIndex].material.color.setHex(0xff00ff);
this.flockLines[connectionIndex].visible = true;
connectionIndex++;
}
});
}
// Following connection
if (this.followTarget && connectionIndex < this.flockLines.length) {
const start = this.mesh.position.clone();
start.y = 3;
const end = this.followTarget.mesh.position.clone();
end.y = 3;
this.flockLines[connectionIndex].geometry.setFromPoints([start, end]);
this.flockLines[connectionIndex].material.color.setHex(0x00ffff);
this.flockLines[connectionIndex].visible = true;
connectionIndex++;
}
// Neighbor connections
this.neighbors.slice(0, 8 - connectionIndex).forEach(neighbor => {
if (connectionIndex < this.flockLines.length) {
const start = this.mesh.position.clone();
start.y = 3;
const end = neighbor.mesh.position.clone();
end.y = 3;
this.flockLines[connectionIndex].geometry.setFromPoints([start, end]);
this.flockLines[connectionIndex].material.color.setHex(0x00ff00);
this.flockLines[connectionIndex].visible = true;
connectionIndex++;
}
});
// Hide unused lines
for (let i = connectionIndex; i < this.flockLines.length; i++) {
this.flockLines[i].visible = false;
}
}
getObstacles() {
let obstacles = [];
population.forEach(car => {
if (car !== this && !car.crashed) {
obstacles.push(car.mesh);
}
});
world.buildings.forEach(building => {
obstacles.push(building.mesh);
});
return obstacles;
}
checkCollisions() {
const carBox = new THREE.Box3().setFromObject(this.mesh);
// Soft collision with other cars
population.forEach(otherCar => {
if (otherCar !== this && !otherCar.crashed && !otherCar.isParked) {
const otherBox = new THREE.Box3().setFromObject(otherCar.mesh);
if (carBox.intersectsBox(otherBox)) {
const separation = new THREE.Vector3()
.subVectors(this.mesh.position, otherCar.mesh.position)
.normalize()
.multiplyScalar(3);
this.velocity.add(separation.multiplyScalar(0.3));
otherCar.velocity.sub(separation.multiplyScalar(0.3));
this.fitness -= 20;
otherCar.fitness -= 20;
this.trafficViolations++;
}
}
});
// Building collisions
world.buildings.forEach(building => {
const buildingBox = new THREE.Box3().setFromObject(building.mesh);
if (carBox.intersectsBox(buildingBox)) {
this.crashed = true;
crashCount++;
}
});
}
keepInBounds() {
const bounds = 400;
if (Math.abs(this.mesh.position.x) > bounds ||
Math.abs(this.mesh.position.z) > bounds) {
if (Math.abs(this.mesh.position.x) > bounds) {
this.mesh.position.x = Math.sign(this.mesh.position.x) * bounds;
this.velocity.x *= -0.6;
}
if (Math.abs(this.mesh.position.z) > bounds) {
this.mesh.position.z = Math.sign(this.mesh.position.z) * bounds;
this.velocity.z *= -0.6;
}
this.fitness -= 10;
}
}
destroy() {
if (this.parkingSpot) {
this.parkingSpot.occupied = false;
this.parkingSpot.car = null;
}
this.flockLines.forEach(line => {
if (line.parent) scene.remove(line);
});
if (this.mesh.parent) {
scene.remove(this.mesh);
}
}
}
function init() {
// Enhanced scene setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 300, 1000);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 150, 150);
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;
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(100, 100, 50);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
createTrafficWorld();
createInitialPopulation();
clock = new THREE.Clock();
// Event listeners
window.addEventListener('resize', onWindowResize);
setupEventListeners();
animate();
}
function createTrafficWorld() {
// Ground
const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
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();
createBuildingsWithParkingLots();
}
function createRoadNetwork() {
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
const lineMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
// Create road grid
for (let i = -300; i <= 300; i += ROAD_SPACING) {
// Horizontal roads
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, i);
scene.add(hRoad);
// Lane dividers for horizontal roads
const hLineGeometry = new THREE.PlaneGeometry(600, 0.3);
const hLine = new THREE.Mesh(hLineGeometry, lineMaterial);
hLine.rotation.x = -Math.PI / 2;
hLine.position.set(0, 0.15, i);
scene.add(hLine);
// Vertical roads
const vRoadGeometry = new THREE.PlaneGeometry(ROAD_WIDTH, 600);
const vRoad = new THREE.Mesh(vRoadGeometry, roadMaterial);
vRoad.rotation.x = -Math.PI / 2;
vRoad.position.set(i, 0.1, 0);
scene.add(vRoad);
// Lane dividers for vertical roads
const vLineGeometry = new THREE.PlaneGeometry(0.3, 600);
const vLine = new THREE.Mesh(vLineGeometry, lineMaterial);
vLine.rotation.x = -Math.PI / 2;
vLine.position.set(i, 0.15, 0);
scene.add(vLine);
}
}
function createBuildingsWithParkingLots() {
world.buildings = [];
world.parkingLots = [];
const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 });
const parkingMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
const spotMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
// Create buildings at strategic locations
const buildingLocations = [
{ x: -75, z: -75 }, { x: 75, z: -75 },
{ x: -75, z: 75 }, { x: 75, z: 75 },
{ x: -225, z: -225 }, { x: 225, z: -225 },
{ x: -225, z: 225 }, { x: 225, z: 225 },
{ x: 0, z: -150 }, { x: 0, z: 150 },
{ x: -150, z: 0 }, { x: 150, z: 0 }
];
buildingLocations.forEach(loc => {
// Create building
const width = 25 + Math.random() * 15;
const height = 15 + Math.random() * 25;
const depth = 25 + Math.random() * 15;
const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
building.position.set(loc.x, height / 2, loc.z);
building.castShadow = true;
scene.add(building);
world.buildings.push({ mesh: building });
// Create large parking lot next to building
const parkingLot = {
center: new THREE.Vector3(loc.x + width/2 + 20, 0.1, loc.z),
spots: []
};
// Parking lot ground
const lotGeometry = new THREE.PlaneGeometry(40, 30);
const lot = new THREE.Mesh(lotGeometry, parkingMaterial);
lot.rotation.x = -Math.PI / 2;
lot.position.copy(parkingLot.center);
scene.add(lot);
// Create parking spots (4x6 grid = 24 spots)
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 6; col++) {
const spotX = parkingLot.center.x + (col - 2.5) * 6;
const spotZ = parkingLot.center.z + (row - 1.5) * 7;
// Spot marking
const spotGeometry = new THREE.PlaneGeometry(PARKING_SPOT_SIZE.width, PARKING_SPOT_SIZE.length);
const spotMesh = new THREE.Mesh(spotGeometry, spotMaterial);
spotMesh.rotation.x = -Math.PI / 2;
spotMesh.position.set(spotX, 0.12, spotZ);
scene.add(spotMesh);
const spot = {
position: new THREE.Vector3(spotX, 1, spotZ),
occupied: false,
car: null,
mesh: spotMesh
};
parkingLot.spots.push(spot);
}
}
world.parkingLots.push(parkingLot);
});
}
function createInitialPopulation() {
population = [];
for (let i = 0; i < populationSize; i++) {
// Start cars on roads when possible
const roadPositions = [
{ x: -280, z: 0 }, { x: 280, z: 0 },
{ x: 0, z: -280 }, { x: 0, z: 280 },
{ x: -130, z: 0 }, { x: 130, z: 0 },
{ x: 0, z: -130 }, { x: 0, z: 130 }
];
const startPos = roadPositions[i % roadPositions.length];
const car = new TrafficCar(
startPos.x + (Math.random() - 0.5) * 10,
startPos.z + (Math.random() - 0.5) * 10
);
population.push(car);
scene.add(car.mesh);
}
}
function evolvePopulation() {
// Sort by fitness
population.sort((a, b) => b.fitness - a.fitness);
// Advanced selection
const eliteCount = Math.floor(populationSize * 0.15);
const tournamentCount = Math.floor(populationSize * 0.25);
const mutatedCount = populationSize - eliteCount - tournamentCount;
const survivors = population.slice(0, eliteCount);
// Tournament selection
for (let i = 0; i < tournamentCount; i++) {
const tournamentSize = 5;
let best = null;
let bestFitness = -Infinity;
for (let j = 0; j < tournamentSize; j++) {
const candidate = population[Math.floor(Math.random() * Math.min(population.length, populationSize * 0.5))];
if (candidate.fitness > bestFitness) {
best = candidate;
bestFitness = candidate.fitness;
}
}
if (best) survivors.push(best);
}
// Clean up old population
population.forEach(car => car.destroy());
// Create new population
const newPopulation = [];
const roadPositions = [
{ x: -280, z: 0 }, { x: 280, z: 0 },
{ x: 0, z: -280 }, { x: 0, z: 280 },
{ x: -130, z: 0 }, { x: 130, z: 0 },
{ x: 0, z: -130 }, { x: 0, z: 130 }
];
// Elite reproduction
survivors.forEach((parent, index) => {
const startPos = roadPositions[index % roadPositions.length];
const newCar = new TrafficCar(
startPos.x + (Math.random() - 0.5) * 10,
startPos.z + (Math.random() - 0.5) * 10
);
newCar.brain = parent.brain.copy();
newPopulation.push(newCar);
scene.add(newCar.mesh);
});
// Mutated offspring
while (newPopulation.length < populationSize) {
const parentIndex = Math.floor(Math.random() * Math.min(survivors.length, eliteCount * 2));
const parent = survivors[parentIndex];
const startPos = roadPositions[newPopulation.length % roadPositions.length];
const child = new TrafficCar(
startPos.x + (Math.random() - 0.5) * 10,
startPos.z + (Math.random() - 0.5) * 10
);
child.brain = parent.brain.copy();
const mutationRate = parent.fitness > bestFitness * 0.8 ? 0.05 : 0.15;
child.brain.mutate(mutationRate);
newPopulation.push(child);
scene.add(child.mesh);
}
population = newPopulation;
// Update epoch
epoch++;
timeLeft = epochTime;
bestFitness = Math.max(bestFitness, survivors[0]?.fitness || 0);
crashCount = 0;
parkingEvents = 0;
laneViolations = 0;
console.log(`Epoch ${epoch}: Best fitness: ${bestFitness.toFixed(1)}, Parking events: ${parkingEvents}`);
}
function animate() {
requestAnimationFrame(animate);
if (!paused) {
const deltaTime = Math.min(clock.getDelta() * speedMultiplier, 0.1);
timeLeft -= deltaTime;
if (timeLeft <= 0) {
evolvePopulation();
}
updatePopulation(deltaTime);
updateCamera();
updateUI();
}
renderer.render(scene, camera);
}
function updatePopulation(deltaTime) {
let stats = {
alive: 0,
leaders: 0,
convoy: 0,
parked: 0,
solo: 0,
maxConvoySize: 0,
totalRoadTime: 0,
totalConvoyTime: 0,
totalParkingScore: 0,
totalViolations: 0,
totalFollowingDistance: 0,
followingCount: 0
};
population.forEach(car => {
car.update(deltaTime);
if (!car.crashed) {
stats.alive++;
stats.totalRoadTime += car.roadTime;
stats.totalConvoyTime += car.convoyTime;
stats.totalParkingScore += car.parkingScore;
stats.totalViolations += car.trafficViolations;
if (car.isParked) {
stats.parked++;
} else if (car.role === 'leader') {
stats.leaders++;
stats.maxConvoySize = Math.max(stats.maxConvoySize, car.convoyFollowers.length + 1);
} else if (car.convoyPosition >= 0) {
stats.convoy++;
if (car.followingDistance > 0) {
stats.totalFollowingDistance += car.followingDistance;
stats.followingCount++;
}
} else {
stats.solo++;
}
}
});
window.populationStats = stats;
}
function updateCamera() {
if (cameraMode === 'follow_best') {
// Follow best performing car
let bestCar = population.reduce((best, car) => {
if (car.crashed || car.isParked) return best;
return !best || car.fitness > best.fitness ? car : best;
}, null);
if (bestCar) {
const targetPos = bestCar.mesh.position.clone();
targetPos.y += 40;
targetPos.add(bestCar.velocity.clone().normalize().multiplyScalar(25));
camera.position.lerp(targetPos, 0.03);
camera.lookAt(bestCar.mesh.position);
}
} else if (cameraMode === 'follow_convoy') {
// Follow largest convoy
let largestConvoy = population.find(car =>
car.role === 'leader' && car.convoyFollowers.length > 0
);
if (largestConvoy) {
const targetPos = largestConvoy.mesh.position.clone();
targetPos.y += 50;
targetPos.add(largestConvoy.velocity.clone().normalize().multiplyScalar(30));
camera.position.lerp(targetPos, 0.03);
camera.lookAt(largestConvoy.mesh.position);
}
} else {
// Overview mode
camera.position.lerp(new THREE.Vector3(0, 180, 180), 0.02);
camera.lookAt(0, 0, 0);
}
}
function updateUI() {
const stats = window.populationStats || {};
// Main UI
document.getElementById('epoch').textContent = epoch;
document.getElementById('epochTime').textContent = Math.ceil(timeLeft);
document.getElementById('population').textContent = stats.alive || 0;
document.getElementById('bestFitness').textContent = Math.round(bestFitness);
// Progress bar
const progress = ((epochTime - timeLeft) / epochTime) * 100;
document.getElementById('timeProgress').style.width = `${progress}%`;
// Traffic stats
if (stats.alive > 0) {
document.getElementById('trafficIQ').textContent = Math.round(50 + (bestFitness / 20));
document.getElementById('roadMastery').textContent = Math.round((stats.totalRoadTime / stats.alive) * 10);
document.getElementById('laneDiscipline').textContent = Math.round(Math.max(0, 100 - (stats.totalViolations / stats.alive) * 10));
document.getElementById('roadAdherence').textContent = Math.round((stats.totalRoadTime / (stats.totalRoadTime + 1)) * 100);
}
// Convoy stats
document.getElementById('leaderCount').textContent = stats.leaders || 0;
document.getElementById('convoyCount').textContent = stats.convoy || 0;
document.getElementById('parkedCount').textContent = stats.parked || 0;
document.getElementById('soloCount').textContent = stats.solo || 0;
document.getElementById('largestConvoy').textContent = stats.maxConvoySize || 0;
// Following distance
if (stats.followingCount > 0) {
document.getElementById('followingDistance').textContent = (stats.totalFollowingDistance / stats.followingCount).toFixed(1);
}
// Generation stats
const totalDistance = population.reduce((sum, car) => sum + car.distanceTraveled, 0);
const maxConvoyLength = Math.max(...population.map(car => car.convoyFollowers?.length || 0));
document.getElementById('totalDistance').textContent = Math.round(totalDistance);
document.getElementById('parkingEvents').textContent = parkingEvents;
document.getElementById('laneViolations').textContent = laneViolations;
document.getElementById('convoyLength').textContent = maxConvoyLength;
document.getElementById('crashCount').textContent = crashCount;
// Parking stats
const totalSpots = world.parkingLots.reduce((sum, lot) => sum + lot.spots.length, 0);
const occupiedSpots = world.parkingLots.reduce((sum, lot) =>
sum + lot.spots.filter(spot => spot.occupied).length, 0);
document.getElementById('spotsOccupied').textContent = occupiedSpots;
document.getElementById('parkingSuccess').textContent = totalSpots > 0 ? Math.round((occupiedSpots / totalSpots) * 100) : 0;
updateTopPerformers();
}
function updateTopPerformers() {
const sorted = [...population]
.filter(car => !car.crashed)
.sort((a, b) => b.fitness - a.fitness)
.slice(0, 5);
const topPerformersDiv = document.getElementById('topPerformers');
topPerformersDiv.innerHTML = '';
sorted.forEach((car, i) => {
const div = document.createElement('div');
const roleIcon = {
leader: 'πŸ‘‘',
parker: 'πŸ…ΏοΈ',
driver: 'πŸš—'
}[car.role] || 'πŸš—';
const statusIcon = car.isParked ? 'πŸ…ΏοΈ' : (car.convoyPosition >= 0 ? 'πŸš›' : 'πŸš—');
div.innerHTML = `${i + 1}. ${roleIcon}${statusIcon} F:${Math.round(car.fitness)} | Lane:${Math.round(car.laneDiscipline * 100)}% | Road:${Math.round(car.roadTime)}s`;
div.className = car.isParked ? 'parked' : (car.role === 'leader' ? 'leader' : (car.convoyPosition >= 0 ? 'convoy' : 'solo'));
topPerformersDiv.appendChild(div);
});
}
function setupEventListeners() {
document.getElementById('pauseBtn').addEventListener('click', togglePause);
document.getElementById('resetBtn').addEventListener('click', resetSimulation);
document.getElementById('speedBtn').addEventListener('click', toggleSpeed);
document.getElementById('viewBtn').addEventListener('click', toggleView);
document.getElementById('flockBtn').addEventListener('click', toggleFlockLines);
document.getElementById('trafficBtn').addEventListener('click', toggleTrafficRules);
}
function togglePause() {
paused = !paused;
document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause';
if (!paused) clock.start();
}
function resetSimulation() {
epoch = 1;
timeLeft = epochTime;
bestFitness = 0;
crashCount = 0;
parkingEvents = 0;
laneViolations = 0;
// Reset parking lots
world.parkingLots.forEach(lot => {
lot.spots.forEach(spot => {
spot.occupied = false;
spot.car = null;
});
});
population.forEach(car => car.destroy());
createInitialPopulation();
}
function toggleSpeed() {
speedMultiplier = speedMultiplier === 1 ? 2 : speedMultiplier === 2 ? 5 : 1;
document.getElementById('speedBtn').textContent = `Speed: ${speedMultiplier}x`;
}
function toggleView() {
const modes = ['overview', 'follow_best', 'follow_convoy'];
const currentIndex = modes.indexOf(cameraMode);
cameraMode = modes[(currentIndex + 1) % modes.length];
const displayNames = {
overview: 'Overview',
follow_best: 'Follow Best',
follow_convoy: 'Follow Convoy'
};
document.getElementById('viewBtn').textContent = `View: ${displayNames[cameraMode]}`;
}
function toggleFlockLines() {
showFlockLines = !showFlockLines;
document.getElementById('flockBtn').textContent = `Networks: ${showFlockLines ? 'ON' : 'OFF'}`;
population.forEach(car => {
car.flockLines.forEach(line => {
if (showFlockLines && !line.parent) {
scene.add(line);
} else if (!showFlockLines && line.parent) {
scene.remove(line);
}
});
});
}
function toggleTrafficRules() {
trafficRules = !trafficRules;
document.getElementById('trafficBtn').textContent = `Traffic Rules: ${trafficRules ? 'ON' : 'OFF'}`;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
init();
</script>
</body>
</html>