ParkingLotDrivingSim / index.html
awacke1's picture
Update index.html
00b2fd6 verified
raw
history blame
109 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
// Road transition tracking
this.lastRoadPosition = 0;
this.isTransitioningToRoad = false;
this.roadTransitionTime = 0;
// 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
// Enhanced parking system
this.isParked = false;
this.parkingSpot = null;
this.targetParkingLot = null;
this.parkingQueue = -1; // Position in parking queue (-1 = not queued)
this.isParkingApproach = false;
this.isInApproachLane = false;
this.isInExitLane = false;
this.approachTarget = null;
this.exitTarget = null;
this.parkingAttempts = 0;
this.maxParkingAttempts = 3;
this.departureTime = 0;
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 major highways (8 lanes - 48 units wide)
const mainHighwayPositions = [0]; // Main cross highways
mainHighwayPositions.forEach(roadPos => {
// Horizontal highway
const distToHorizontal = Math.abs(pos.z - roadPos);
if (distToHorizontal < 24 && distToHorizontal < minDistance) {
minDistance = distToHorizontal;
const laneCenter = roadPos + (pos.x > 0 ? -12 : 12); // 4 lanes each direction
nearestRoad = {
lane: 'highway_horizontal',
center: laneCenter,
direction: pos.x > 0 ? Math.PI : 0,
width: 48
};
}
// Vertical highway
const distToVertical = Math.abs(pos.x - roadPos);
if (distToVertical < 24 && distToVertical < minDistance) {
minDistance = distToVertical;
const laneCenter = roadPos + (pos.z > 0 ? -12 : 12);
nearestRoad = {
lane: 'highway_vertical',
center: laneCenter,
direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2,
width: 48
};
}
});
// Check secondary highways (4 lanes - 24 units wide)
for (let roadPos = -300; roadPos <= 300; roadPos += 150) {
if (roadPos === 0) continue; // Skip main highway
const distToHorizontal = Math.abs(pos.z - roadPos);
if (distToHorizontal < 12 && distToHorizontal < minDistance) {
minDistance = distToHorizontal;
const laneCenter = roadPos + (pos.x > 0 ? -6 : 6);
nearestRoad = {
lane: 'secondary_horizontal',
center: laneCenter,
direction: pos.x > 0 ? Math.PI : 0,
width: 24
};
}
const distToVertical = Math.abs(pos.x - roadPos);
if (distToVertical < 12 && distToVertical < minDistance) {
minDistance = distToVertical;
const laneCenter = roadPos + (pos.z > 0 ? -6 : 6);
nearestRoad = {
lane: 'secondary_vertical',
center: laneCenter,
direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2,
width: 24
};
}
}
// Check local roads (2 lanes - 12 units wide)
for (let roadPos = -375; roadPos <= 375; roadPos += 75) {
const distToHorizontal = Math.abs(pos.z - roadPos);
if (distToHorizontal < 6 && distToHorizontal < minDistance) {
minDistance = distToHorizontal;
const laneCenter = roadPos + (pos.x > 0 ? -3 : 3);
nearestRoad = {
lane: 'local_horizontal',
center: laneCenter,
direction: pos.x > 0 ? Math.PI : 0,
width: 12
};
}
const distToVertical = Math.abs(pos.x - roadPos);
if (distToVertical < 6 && distToVertical < minDistance) {
minDistance = distToVertical;
const laneCenter = roadPos + (pos.z > 0 ? -3 : 3);
nearestRoad = {
lane: 'local_vertical',
center: laneCenter,
direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2,
width: 12
};
}
}
// Check building access roads (less precise matching)
if (!nearestRoad || minDistance > 8) {
world.buildings.forEach(building => {
const buildingPos = building.mesh.position;
const buildingBox = new THREE.Box3().setFromObject(building.mesh);
const size = buildingBox.getSize(new THREE.Vector3());
// Check proximity to building access roads
const accessRoadPositions = [
{ x: buildingPos.x, z: buildingPos.z + size.z/2 + 10, dir: 'horizontal' },
{ x: buildingPos.x, z: buildingPos.z - size.z/2 - 10, dir: 'horizontal' },
{ x: buildingPos.x + size.x/2 + 10, z: buildingPos.z, dir: 'vertical' },
{ x: buildingPos.x - size.x/2 - 10, z: buildingPos.z, dir: 'vertical' }
];
accessRoadPositions.forEach(accessRoad => {
const dist = pos.distanceTo(new THREE.Vector3(accessRoad.x, 0, accessRoad.z));
if (dist < 8 && dist < minDistance) {
minDistance = dist;
nearestRoad = {
lane: `access_${accessRoad.dir}`,
center: accessRoad.dir === 'horizontal' ? accessRoad.z : accessRoad.x,
direction: accessRoad.dir === 'horizontal' ?
(pos.x > accessRoad.x ? Math.PI : 0) :
(pos.z > accessRoad.z ? -Math.PI/2 : Math.PI/2),
width: 8
};
}
});
});
}
return nearestRoad;
}
getRoadPosition() {
const pos = this.mesh.position;
let maxRoadScore = 0;
// Check all road types for best road position score
// Major highways
const mainHighwayDist = Math.abs(pos.z);
if (mainHighwayDist <= 24) {
maxRoadScore = Math.max(maxRoadScore, 1 - (mainHighwayDist / 24));
}
const mainVerticalDist = Math.abs(pos.x);
if (mainVerticalDist <= 24) {
maxRoadScore = Math.max(maxRoadScore, 1 - (mainVerticalDist / 24));
}
// Secondary highways
for (let roadPos = -300; roadPos <= 300; roadPos += 150) {
if (roadPos === 0) continue;
const hDist = Math.abs(pos.z - roadPos);
if (hDist <= 12) {
maxRoadScore = Math.max(maxRoadScore, 1 - (hDist / 12));
}
const vDist = Math.abs(pos.x - roadPos);
if (vDist <= 12) {
maxRoadScore = Math.max(maxRoadScore, 1 - (vDist / 12));
}
}
// Local roads
for (let roadPos = -375; roadPos <= 375; roadPos += 75) {
const hDist = Math.abs(pos.z - roadPos);
if (hDist <= 6) {
maxRoadScore = Math.max(maxRoadScore, 1 - (hDist / 6));
}
const vDist = Math.abs(pos.x - roadPos);
if (vDist <= 6) {
maxRoadScore = Math.max(maxRoadScore, 1 - (vDist / 6));
}
}
return maxRoadScore;
}
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);
}
}
});
// Calculate flock centroid and check if moving away
this.updateFlockCohesion();
// Determine role and convoy behavior
this.updateRole();
this.updateConvoyFormation();
}
updateFlockCohesion() {
if (this.neighbors.length === 0) return;
// Calculate flock centroid
const centroid = new THREE.Vector3();
this.neighbors.forEach(neighbor => {
centroid.add(neighbor.mesh.position);
});
centroid.divideScalar(this.neighbors.length);
// Check if moving away from centroid
const currentPos = this.mesh.position.clone();
const futurePos = currentPos.clone().add(this.velocity.clone().normalize().multiplyScalar(5));
const currentDistToCentroid = currentPos.distanceTo(centroid);
const futureDistToCentroid = futurePos.distanceTo(centroid);
// If moving away from flock, apply gentle correction
if (futureDistToCentroid > currentDistToCentroid && currentDistToCentroid > 15) {
const returnForce = centroid.clone().sub(currentPos).normalize().multiplyScalar(0.3);
this.velocity.add(returnForce);
// Gentle turning toward centroid
const targetDirection = Math.atan2(returnForce.x, returnForce.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;
// Apply gentle turning
this.mesh.rotation.y += angleDiff * 0.02;
// Slow down for turning
this.velocity.multiplyScalar(0.98);
}
// Move flock centroid toward roads
this.guideFlockToRoads(centroid);
}
guideFlockToRoads(centroid) {
if (this.role !== 'leader') return;
// Find nearest road from centroid
const nearestRoadInfo = this.findNearestRoadFromPosition(centroid);
if (!nearestRoadInfo) return;
const roadDistance = nearestRoadInfo.distance;
if (roadDistance > 5) {
// Guide flock toward road
const roadDirection = nearestRoadInfo.direction;
const roadForce = roadDirection.clone().multiplyScalar(0.2);
this.velocity.add(roadForce);
}
}
findNearestRoadFromPosition(position) {
let nearestRoad = null;
let minDistance = Infinity;
// Check horizontal roads
for (let roadZ = -300; roadZ <= 300; roadZ += ROAD_SPACING) {
const distToRoad = Math.abs(position.z - roadZ);
if (distToRoad < minDistance) {
minDistance = distToRoad;
const direction = new THREE.Vector3(0, 0, roadZ - position.z).normalize();
nearestRoad = {
distance: distToRoad,
direction: direction,
lane: 'horizontal',
center: roadZ
};
}
}
// Check vertical roads
for (let roadX = -300; roadX <= 300; roadX += ROAD_SPACING) {
const distToRoad = Math.abs(position.x - roadX);
if (distToRoad < minDistance) {
minDistance = distToRoad;
const direction = new THREE.Vector3(roadX - position.x, 0, 0).normalize();
nearestRoad = {
distance: distToRoad,
direction: direction,
lane: 'vertical',
center: roadX
};
}
}
return nearestRoad;
}
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) {
// Handle parked cars separately
if (this.isParked) {
this.handleParkedBehavior(deltaTime);
return;
}
if (this.crashed) return;
this.timeAlive -= deltaTime;
if (this.timeAlive <= 0 || this.parkingAttempts >= this.maxParkingAttempts) {
if (!this.isParkingApproach) {
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();
const currentRoadPos = this.getRoadPosition();
// Check if transitioning from grass to road
if (currentRoadPos > 0.3 && this.lastRoadPosition <= 0.3) {
// Just touched road after being on grass - slow down
this.velocity.multiplyScalar(0.7);
this.isTransitioningToRoad = true;
this.roadTransitionTime = 2.0; // 2 seconds to stabilize
}
if (this.roadTransitionTime > 0) {
this.roadTransitionTime -= deltaTime;
// Maintain slower speed during transition
const currentSpeed = this.velocity.length();
if (currentSpeed > this.maxSpeed * 0.6) {
this.velocity.normalize().multiplyScalar(this.maxSpeed * 0.6);
}
}
this.lastRoadPosition = currentRoadPos;
if (!roadInfo) return;
// Enhanced lane-keeping force
const laneKeepingForce = this.brain.trafficTraits.laneKeeping;
let targetDirection = roadInfo.direction;
// Adjust for lane position with stronger correction
if (roadInfo.lane === 'horizontal') {
const laneOffset = this.mesh.position.z - roadInfo.center;
if (Math.abs(laneOffset) > LANE_WIDTH / 4) {
const correction = -laneOffset * 0.05 * 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.05 * laneKeepingForce;
this.velocity.x += correction;
if (Math.abs(laneOffset) > LANE_WIDTH / 2) {
this.trafficViolations++;
laneViolations++;
}
}
}
// Stronger direction alignment for road following
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.08 * 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) {
this.findNearestParkingLot();
return;
}
// Enhanced parking state machine
if (this.parkingQueue === -1) {
this.joinParkingQueue();
}
if (this.isInApproachLane) {
this.handleApproachLane(deltaTime);
} else if (this.canProceedToApproach()) {
this.enterApproachLane(deltaTime);
} else {
this.waitForApproachAccess(deltaTime);
}
}
joinParkingQueue() {
if (!this.targetParkingLot) return;
if (!this.targetParkingLot.queue) {
this.targetParkingLot.queue = [];
}
if (!this.targetParkingLot.queue.includes(this)) {
this.targetParkingLot.queue.push(this);
this.parkingQueue = this.targetParkingLot.queue.length - 1;
}
}
canProceedToApproach() {
if (!this.targetParkingLot || !this.targetParkingLot.queue) return false;
const queuePosition = this.targetParkingLot.queue.indexOf(this);
// Check if there's space in approach lane
const approachLaneOccupancy = population.filter(car =>
car.isInApproachLane && car.targetParkingLot === this.targetParkingLot
).length;
return queuePosition < 3 && approachLaneOccupancy < this.targetParkingLot.approachLane.length;
}
enterApproachLane(deltaTime) {
// Find first available approach lane position
const approachPositions = this.targetParkingLot.approachLane;
let targetPosition = null;
for (let i = 0; i < approachPositions.length; i++) {
const pos = approachPositions[i];
const occupied = population.some(car =>
car !== this &&
car.isInApproachLane &&
car.mesh.position.distanceTo(pos) < 3
);
if (!occupied) {
targetPosition = pos;
break;
}
}
if (targetPosition) {
this.isInApproachLane = true;
this.approachTarget = targetPosition;
this.moveToPosition(targetPosition, deltaTime, 4); // Slow approach
}
}
handleApproachLane(deltaTime) {
// Check if we can proceed to actual parking
const availableSpot = this.targetParkingLot.spots.find(spot => !spot.occupied);
if (!availableSpot) {
// Wait in approach lane
this.velocity.multiplyScalar(0.95);
return;
}
const spotDistance = this.mesh.position.distanceTo(availableSpot.position);
if (spotDistance < 3) {
// Successfully park
this.completeParkingProcess(availableSpot);
} else {
// Move toward spot
this.moveToPosition(availableSpot.position, deltaTime, 2);
}
}
completeParkingProcess(spot) {
this.isParked = true;
this.parkingSpot = spot;
spot.occupied = true;
spot.car = this;
this.mesh.position.copy(spot.position);
this.velocity.set(0, 0, 0);
this.parkingScore += 100;
this.leaveParkingQueue();
this.isInApproachLane = false;
parkingEvents++;
this.departureTime = 15 + Math.random() * 25;
this.updateCarColor();
}
waitForApproachAccess(deltaTime) {
// Line up end-to-end near parking lot
const queuePosition = Math.min(this.parkingQueue, 5);
const queueTarget = this.targetParkingLot.center.clone();
queueTarget.add(new THREE.Vector3(-50, 0, -15 + queuePosition * 5)); // 5m spacing
this.moveToPosition(queueTarget, deltaTime, 3);
}
moveToPosition(targetPos, deltaTime, speed) {
const direction = targetPos.clone().sub(this.mesh.position);
const distance = direction.length();
if (distance > 1) {
direction.normalize();
this.velocity.copy(direction.multiplyScalar(speed));
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
// Align rotation with movement
const targetAngle = Math.atan2(direction.x, direction.z);
this.mesh.rotation.y = targetAngle;
} else {
this.velocity.multiplyScalar(0.9);
}
}
leaveParking() {
if (!this.isParked || !this.parkingSpot) return;
// Free the parking spot
this.parkingSpot.occupied = false;
this.parkingSpot.car = null;
this.parkingSpot = null;
this.isParked = false;
this.isInApproachLane = false;
// Use exit lane for organized departure
this.useExitLane();
}
useExitLane() {
// Find exit lane position
const exitPositions = this.targetParkingLot.exitLane;
let exitTarget = null;
for (let i = 0; i < exitPositions.length; i++) {
const pos = exitPositions[i];
const occupied = population.some(car =>
car !== this &&
car.mesh.position.distanceTo(pos) < 4
);
if (!occupied) {
exitTarget = pos;
break;
}
}
if (exitTarget) {
this.isInExitLane = true;
this.exitTarget = exitTarget;
this.mesh.position.copy(exitTarget);
// Set exit velocity
const exitDirection = new THREE.Vector3(0, 0, 1); // Move south to exit
this.velocity.copy(exitDirection.multiplyScalar(6));
// Schedule exit lane departure
setTimeout(() => {
this.isInExitLane = false;
this.role = 'driver';
this.timeAlive = 50 + Math.random() * 30;
this.updateCarColor();
}, 2000);
}
}
attemptParking() {
this.role = 'parker';
this.findNearestParkingLot();
if (!this.targetParkingLot) {
// No parking available, become a wanderer
this.timeAlive = 20;
this.role = 'driver';
return;
}
// Start parking process
this.isParkingApproach = 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);
// Modified collision behavior for parking areas
const isParkingMode = this.isInApproachLane || this.isInExitLane ||
this.isParkingApproach || this.role === 'parker';
// Soft collision with other cars
population.forEach(otherCar => {
if (otherCar !== this && !otherCar.crashed && !otherCar.isParked) {
const otherBox = new THREE.Box3().setFromObject(otherCar.mesh);
const distance = this.mesh.position.distanceTo(otherCar.mesh.position);
// Relaxed collision detection for parking behavior
const minDistance = isParkingMode || otherCar.isInApproachLane ? 2.5 : 4.0;
const bothInParkingMode = isParkingMode &&
(otherCar.isInApproachLane || otherCar.isParkingApproach || otherCar.role === 'parker');
if (carBox.intersectsBox(otherBox) || (distance < minDistance && !bothInParkingMode)) {
// Gentle separation instead of hard collision
const separation = new THREE.Vector3()
.subVectors(this.mesh.position, otherCar.mesh.position)
.normalize();
if (isParkingMode) {
// Very gentle separation for parking
separation.multiplyScalar(1.5);
this.velocity.add(separation.multiplyScalar(0.1));
this.velocity.multiplyScalar(0.95); // Slow down
} else {
// Normal separation
separation.multiplyScalar(3);
this.velocity.add(separation.multiplyScalar(0.3));
otherCar.velocity.sub(separation.multiplyScalar(0.3));
this.fitness -= 10;
otherCar.fitness -= 10;
this.trafficViolations++;
}
}
}
});
// Building collisions (unchanged)
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() {
// Clean up parking spot
if (this.parkingSpot) {
this.parkingSpot.occupied = false;
this.parkingSpot.car = null;
}
// Remove from parking queue
this.leaveParkingQueue();
// Clean up visual elements
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() {
// Enhanced ground with road texture hints
const groundGeometry = new THREE.PlaneGeometry(1200, 1200);
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);
// Create comprehensive road network first
createRoadNetwork();
// Then create buildings with parking lots
createBuildingsWithParkingLots();
}
function createRoadNetwork() {
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
const highwayMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
const lineMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
const yellowLineMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
// Create comprehensive road network
// 1. MAJOR HIGHWAYS (8 lanes) - Cross patterns
createHighway(-400, 400, 0, 'horizontal', 48, highwayMaterial, yellowLineMaterial); // North highway
createHighway(-400, 400, 0, 'vertical', 48, highwayMaterial, yellowLineMaterial); // Main highway
// 2. SECONDARY HIGHWAYS (4 lanes) - Grid system
for (let i = -300; i <= 300; i += 150) {
if (i !== 0) { // Don't overlap main highway
createHighway(-400, 400, i, 'horizontal', 24, roadMaterial, yellowLineMaterial);
createHighway(-400, 400, i, 'vertical', 24, roadMaterial, yellowLineMaterial);
}
}
// 3. LOCAL ROADS (2 lanes) - Connecting roads
for (let i = -375; i <= 375; i += 75) {
createLocalRoad(-400, 400, i, 'horizontal', 12, roadMaterial, lineMaterial);
createLocalRoad(-400, 400, i, 'vertical', 12, roadMaterial, lineMaterial);
}
// 4. BUILDING ACCESS ROADS - Around all buildings
createBuildingAccessRoads();
}
function createHighway(start, end, position, direction, width, material, linemat) {
if (direction === 'horizontal') {
// Main road surface
const roadGeometry = new THREE.PlaneGeometry(end - start, width);
const road = new THREE.Mesh(roadGeometry, material);
road.rotation.x = -Math.PI / 2;
road.position.set((start + end) / 2, 0.1, position);
scene.add(road);
// Lane dividers
const laneCount = width / 6; // 6 units per lane
for (let lane = 1; lane < laneCount; lane++) {
const lineY = position - width/2 + (lane * 6);
// Center divider (yellow)
if (lane === Math.floor(laneCount / 2)) {
createDashedLine(start, end, lineY, 'horizontal', linemat, true);
} else {
// Regular lane dividers (white)
createDashedLine(start, end, lineY, 'horizontal', linemat, false);
}
}
} else {
// Vertical highway
const roadGeometry = new THREE.PlaneGeometry(width, end - start);
const road = new THREE.Mesh(roadGeometry, material);
road.rotation.x = -Math.PI / 2;
road.position.set(position, 0.1, (start + end) / 2);
scene.add(road);
// Lane dividers
const laneCount = width / 6;
for (let lane = 1; lane < laneCount; lane++) {
const lineX = position - width/2 + (lane * 6);
if (lane === Math.floor(laneCount / 2)) {
createDashedLine(start, end, lineX, 'vertical', linemat, true);
} else {
createDashedLine(start, end, lineX, 'vertical', linemat, false);
}
}
}
}
function createLocalRoad(start, end, position, direction, width, material, linemat) {
if (direction === 'horizontal') {
const roadGeometry = new THREE.PlaneGeometry(end - start, width);
const road = new THREE.Mesh(roadGeometry, material);
road.rotation.x = -Math.PI / 2;
road.position.set((start + end) / 2, 0.1, position);
scene.add(road);
// Center line
createDashedLine(start, end, position, 'horizontal', linemat, false);
} else {
const roadGeometry = new THREE.PlaneGeometry(width, end - start);
const road = new THREE.Mesh(roadGeometry, material);
road.rotation.x = -Math.PI / 2;
road.position.set(position, 0.1, (start + end) / 2);
scene.add(road);
// Center line
createDashedLine(start, end, position, 'vertical', linemat, false);
}
}
function createDashedLine(start, end, position, direction, material, isDouble) {
const dashLength = 8;
const gapLength = 4;
const totalLength = end - start;
const segments = Math.floor(totalLength / (dashLength + gapLength));
for (let i = 0; i < segments; i++) {
const segmentStart = start + i * (dashLength + gapLength);
if (direction === 'horizontal') {
const lineGeometry = new THREE.PlaneGeometry(dashLength, 0.3);
const line = new THREE.Mesh(lineGeometry, material);
line.rotation.x = -Math.PI / 2;
line.position.set(segmentStart + dashLength/2, 0.12, position);
scene.add(line);
// Double line for highway center
if (isDouble) {
const line2 = new THREE.Mesh(lineGeometry, material);
line2.rotation.x = -Math.PI / 2;
line2.position.set(segmentStart + dashLength/2, 0.12, position + 1);
scene.add(line2);
}
} else {
const lineGeometry = new THREE.PlaneGeometry(0.3, dashLength);
const line = new THREE.Mesh(lineGeometry, material);
line.rotation.x = -Math.PI / 2;
line.position.set(position, 0.12, segmentStart + dashLength/2);
scene.add(line);
if (isDouble) {
const line2 = new THREE.Mesh(lineGeometry, material);
line2.rotation.x = -Math.PI / 2;
line2.position.set(position + 1, 0.12, segmentStart + dashLength/2);
scene.add(line2);
}
}
}
}
function createBuildingAccessRoads() {
const accessRoadMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 });
const lineMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
world.buildings.forEach((building, index) => {
const pos = building.mesh.position;
const buildingBox = new THREE.Box3().setFromObject(building.mesh);
const size = buildingBox.getSize(new THREE.Vector3());
// Create access roads around each building (rectangular loop)
const roadWidth = 8;
const buffer = 5; // Distance from building
// North road
createBuildingRoad(
pos.x - size.x/2 - buffer - 10,
pos.x + size.x/2 + buffer + 10,
pos.z + size.z/2 + buffer + roadWidth/2,
'horizontal', roadWidth, accessRoadMaterial, lineMaterial
);
// South road
createBuildingRoad(
pos.x - size.x/2 - buffer - 10,
pos.x + size.x/2 + buffer + 10,
pos.z - size.z/2 - buffer - roadWidth/2,
'horizontal', roadWidth, accessRoadMaterial, lineMaterial
);
// East road
createBuildingRoad(
pos.x + size.x/2 + buffer + roadWidth/2,
pos.z - size.z/2 - buffer - 10,
pos.z + size.z/2 + buffer + 10,
'vertical', roadWidth, accessRoadMaterial, lineMaterial
);
// West road
createBuildingRoad(
pos.x - size.x/2 - buffer - roadWidth/2,
pos.z - size.z/2 - buffer - 10,
pos.z + size.z/2 + buffer + 10,
'vertical', roadWidth, accessRoadMaterial, lineMaterial
);
// Connecting roads to main network
createConnectorRoads(pos, size, roadWidth, accessRoadMaterial);
});
}
function createBuildingRoad(startOrX, endOrStartZ, positionOrEndZ, direction, width, material, linemat) {
if (direction === 'horizontal') {
const roadGeometry = new THREE.PlaneGeometry(endOrStartZ - startOrX, width);
const road = new THREE.Mesh(roadGeometry, material);
road.rotation.x = -Math.PI / 2;
road.position.set((startOrX + endOrStartZ) / 2, 0.08, positionOrEndZ);
scene.add(road);
// Center line
const lineGeometry = new THREE.PlaneGeometry(endOrStartZ - startOrX, 0.2);
const line = new THREE.Mesh(lineGeometry, linemat);
line.rotation.x = -Math.PI / 2;
line.position.set((startOrX + endOrStartZ) / 2, 0.1, positionOrEndZ);
scene.add(line);
} else {
const roadGeometry = new THREE.PlaneGeometry(width, positionOrEndZ - endOrStartZ);
const road = new THREE.Mesh(roadGeometry, material);
road.rotation.x = -Math.PI / 2;
road.position.set(startOrX, 0.08, (endOrStartZ + positionOrEndZ) / 2);
scene.add(road);
// Center line
const lineGeometry = new THREE.PlaneGeometry(0.2, positionOrEndZ - endOrStartZ);
const line = new THREE.Mesh(lineGeometry, linemat);
line.rotation.x = -Math.PI / 2;
line.position.set(startOrX, 0.1, (endOrStartZ + positionOrEndZ) / 2);
scene.add(line);
}
}
function createConnectorRoads(buildingPos, buildingSize, roadWidth, material) {
// Connect building access roads to nearest main roads
const nearestMainRoads = findNearestMainRoads(buildingPos);
nearestMainRoads.forEach(mainRoad => {
// Create connector from building to main road
const startPos = buildingPos.clone();
const endPos = mainRoad.position.clone();
// Create straight connector road
const distance = startPos.distanceTo(endPos);
const direction = endPos.clone().sub(startPos).normalize();
const connectorGeometry = new THREE.PlaneGeometry(roadWidth, distance);
const connector = new THREE.Mesh(connectorGeometry, material);
connector.rotation.x = -Math.PI / 2;
connector.rotation.z = Math.atan2(direction.x, direction.z);
connector.position.copy(startPos.clone().add(endPos).multiplyScalar(0.5));
connector.position.y = 0.08;
scene.add(connector);
});
}
function findNearestMainRoads(position) {
const mainRoads = [];
// Find nearest horizontal main road
let nearestHorizontalDist = Infinity;
let nearestHorizontalZ = 0;
for (let z = -300; z <= 300; z += 150) {
const dist = Math.abs(position.z - z);
if (dist < nearestHorizontalDist) {
nearestHorizontalDist = dist;
nearestHorizontalZ = z;
}
}
if (nearestHorizontalDist < 100) {
mainRoads.push({
position: new THREE.Vector3(position.x, 0, nearestHorizontalZ),
type: 'horizontal'
});
}
// Find nearest vertical main road
let nearestVerticalDist = Infinity;
let nearestVerticalX = 0;
for (let x = -300; x <= 300; x += 150) {
const dist = Math.abs(position.x - x);
if (dist < nearestVerticalDist) {
nearestVerticalDist = dist;
nearestVerticalX = x;
}
}
if (nearestVerticalDist < 100) {
mainRoads.push({
position: new THREE.Vector3(nearestVerticalX, 0, position.z),
type: 'vertical'
});
}
return mainRoads.slice(0, 2); // Maximum 2 connections per building
}
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 });
const queueMaterial = new THREE.MeshLambertMaterial({ color: 0x222222 });
// Create buildings at strategic locations with better spacing
const buildingLocations = [
{ x: -200, z: -200 }, { x: 0, z: -200 }, { x: 200, z: -200 },
{ x: -200, z: 0 }, { x: 200, z: 0 },
{ x: -200, z: 200 }, { x: 0, z: 200 }, { x: 200, z: 200 },
{ x: -100, z: -100 }, { x: 100, z: -100 },
{ x: -100, z: 100 }, { x: 100, z: 100 },
{ x: -300, z: -300 }, { x: 300, z: -300 },
{ x: -300, z: 300 }, { x: 300, z: 300 }
];
buildingLocations.forEach((loc, index) => {
// Create building
const width = 20 + Math.random() * 10;
const height = 12 + Math.random() * 20;
const depth = 20 + Math.random() * 10;
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 enhanced parking lot with approach lanes
const parkingLot = {
center: new THREE.Vector3(loc.x + width/2 + 25, 0.1, loc.z),
spots: [],
queue: [],
approachLane: [],
exitLane: []
};
// Main parking lot surface (larger)
const lotGeometry = new THREE.PlaneGeometry(50, 40);
const lot = new THREE.Mesh(lotGeometry, parkingMaterial);
lot.rotation.x = -Math.PI / 2;
lot.position.copy(parkingLot.center);
scene.add(lot);
// Approach lane (single file entry)
const approachGeometry = new THREE.PlaneGeometry(6, 60);
const approachLane = new THREE.Mesh(approachGeometry, queueMaterial);
approachLane.rotation.x = -Math.PI / 2;
approachLane.position.set(parkingLot.center.x - 30, 0.08, parkingLot.center.z);
scene.add(approachLane);
// Exit lane (single file exit)
const exitGeometry = new THREE.PlaneGeometry(6, 60);
const exitLane = new THREE.Mesh(exitGeometry, queueMaterial);
exitLane.rotation.x = -Math.PI / 2;
exitLane.position.set(parkingLot.center.x + 30, 0.08, parkingLot.center.z);
scene.add(exitLane);
// Create approach queue positions
for (let q = 0; q < 12; q++) {
const queuePos = new THREE.Vector3(
parkingLot.center.x - 30,
1,
parkingLot.center.z - 25 + (q * 4.5) // 4.5m spacing for tight queuing
);
parkingLot.approachLane.push(queuePos);
}
// Create exit queue positions
for (let q = 0; q < 8; q++) {
const exitPos = new THREE.Vector3(
parkingLot.center.x + 30,
1,
parkingLot.center.z - 15 + (q * 4) // Tighter exit spacing
);
parkingLot.exitLane.push(exitPos);
}
// Create parking spots (5x8 grid = 40 spots)
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 8; col++) {
const spotX = parkingLot.center.x + (col - 3.5) * 6;
const spotZ = parkingLot.center.z + (row - 2) * 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,
approaching: 0 // Cars approaching parking
};
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.isParkingApproach) {
stats.approaching++;
} 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 and queues
world.parkingLots.forEach(lot => {
lot.spots.forEach(spot => {
spot.occupied = false;
spot.car = null;
});
lot.queue = []; // Clear parking queues
lot.approachLane = lot.approachLane || [];
lot.exitLane = lot.exitLane || [];
});
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>