ParkingLotDrivingSim / index.html
awacke1's picture
Google Gemini version
5c871bf verified
<!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: 220px;
}
#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 Visits: <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: [], // Will store { mesh: buildingMesh, parkingLot: parkingLotObject, visitorCount: 0, barGraphMesh: barMesh }
parkingLots: [], // Will store { center, spots, queue, approachLanes, exitLanes, accessPoints, building: buildingObject }
flockLines: []
};
// Enhanced evolution system
let epoch = 1;
let epochTime = 60; // seconds per epoch
let timeLeft = 60;
let population = [];
let species = []; // For future speciation if needed
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; // General toggle, specific rules handled by AI traits
let parkingEvents = 0; // Total parking visits in an epoch
let laneViolations = 0; // Total lane violations in an epoch
// Traffic and road parameters
const ROAD_WIDTH_UNIT = 6; // Base width for one lane
const ROAD_SPACING = 150; // Spacing for major grid roads
const FOLLOW_DISTANCE = 8; // Base follow distance for convoys
const CONVOY_MAX_DISTANCE = 12; // Max distance before convoy link breaks
const PARKING_SPOT_SIZE = { width: 4, length: 8 };
const GRASS_THRESHOLD = 0.15; // Road position value below which car is considered on grass
// Manual control state for "Follow Best"
let manuallyControlledCar = null;
const manualControls = { W: false, A: false, S: false, D: false };
// Enhanced Neural Network for traffic behavior
class TrafficAI {
constructor() {
this.inputSize = 28; // Enhanced traffic-aware inputs
this.hiddenLayers = [36, 28, 20]; // Hidden layer sizes
this.outputSize = 10; // Outputs: accel, brake, steerL, steerR, laneChange, convoy, park, signalL, signalR, stop
this.memorySize = 8; // Short-term memory for road context
this.weights = [];
this.biases = [];
this.memory = new Array(this.memorySize).fill(0);
this.memoryPointer = 0;
// Build network layers
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 (evolvable)
this.trafficTraits = {
laneKeeping: Math.random(), // 0-1, tendency to stay in lane
followingBehavior: Math.random(), // 0-1, how closely to follow
parkingSkill: Math.random(), // 0-1, efficiency in parking
convoyDiscipline: Math.random(), // 0-1, tendency to form/join convoys
roadPriority: Math.random() // 0-1, preference for staying on roads
};
}
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; // Weights between -1 and 1
}
}
return matrix;
}
randomArray(size) {
return Array(size).fill().map(() => (Math.random() - 0.5) * 2); // Biases between -1 and 1
}
activate(inputs) {
let currentInput = [...inputs, ...this.memory]; // Combine current inputs with memory
// Forward pass through hidden layers
for (let layer = 0; layer < this.hiddenLayers.length; layer++) {
currentInput = this.forwardLayer(currentInput, this.weights[layer], this.biases[layer]);
}
// Output layer
const outputs = this.forwardLayer(currentInput,
this.weights[this.weights.length - 1],
this.biases[this.biases.length - 1]);
this.updateMemory(inputs, outputs); // Update memory based on current state
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]); // Sigmoid activation
}
return outputs;
}
sigmoid(x) {
// Clamping input to prevent extreme values in exp, which can cause NaN
const clampedX = Math.max(-10, Math.min(10, x));
return 1 / (1 + Math.exp(-clampedX));
}
updateMemory(inputs, outputs) {
// Example: Store average road sensor data in memory
const roadInfo = inputs.slice(20, 24).reduce((a, b) => a + b, 0) / 4; // Average of road sensors
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);
});
// Mutate traffic traits
Object.keys(this.trafficTraits).forEach(trait => {
if (Math.random() < rate) {
this.trafficTraits[trait] += (Math.random() - 0.5) * 0.2; // Small random change
this.trafficTraits[trait] = Math.max(0, Math.min(1, this.trafficTraits[trait])); // Clamp between 0 and 1
}
});
}
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; // Small random change
matrix[i][j] = Math.max(-3, Math.min(3, matrix[i][j])); // Clamp weights
}
}
}
}
mutateArray(array, rate) {
for (let i = 0; i < array.length; i++) {
if (Math.random() < rate) {
array[i] += (Math.random() - 0.5) * 0.5; // Small random change
array[i] = Math.max(-3, Math.min(3, array[i])); // Clamp biases
}
}
}
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}; // Copy traits
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); // Car height above ground
// Movement and traffic properties
this.velocity = new THREE.Vector3();
this.acceleration = new THREE.Vector3(); // Not directly used, NN outputs control velocity changes
this.maxSpeed = 20; // Max speed units per second
this.minSpeed = 2; // Min speed when moving
this.currentLane = null; // Reference to current road lane object (if any)
this.targetLane = null; // Target lane for lane changes
this.lanePosition = 0; // -1 (left edge) to 1 (right edge) within its current lane
// Road transition tracking
this.lastRoadPositionScore = 0; // Score from getRoadPosition() in previous frame
this.isReturningToRoad = false; // Flag for 180-turn behavior when on grass
this.turnAngleGoal = 0; // Target angle for the turn
this.turnProgress = 0; // Current progress of the turn
this.initialOrientationY = 0; // Orientation before starting a turn
// Convoy and flock behavior
this.flockId = -1; // ID for flocking group (future use)
this.convoyPosition = -1; // Position in convoy (-1 = not in convoy, 0 = leader)
this.convoyLeader = null; // Reference to convoy leader car
this.convoyFollowers = []; // Array of cars following this one (if leader)
this.followTarget = null; // Car this one is following in a convoy
this.role = 'driver'; // 'driver', 'leader', 'parker'
// Enhanced parking system
this.isParked = false;
this.parkingSpot = null; // Reference to the ParkingSpot object
this.targetParkingLot = null; // Reference to the ParkingLot object
this.parkingQueuePosition = -1; // Position in parking lot queue
this.isParkingApproach = false; // True if actively moving towards a parking spot
this.isInApproachLane = false; // True if in a dedicated approach lane
this.isInExitLane = false; // True if in a dedicated exit lane
this.approachTargetPosition = null; // Specific point in approach lane
this.exitTargetPosition = null; // Specific point in exit lane
this.selectedApproachLaneIndex = -1;
this.selectedExitLaneIndex = -1;
this.parkingAttempts = 0; // Number of times tried to park this epoch
this.maxParkingAttempts = 3;
this.departureTime = 0; // Timer for how long to stay parked
this.isExitingParking = false; // Flag for 180-degree turn when exiting parking
this.turnSignal = 'none'; // 'left', 'right', 'none'
this.laneDiscipline = 0; // Score for staying in lane
this.followingDistance = FOLLOW_DISTANCE; // Current following distance
// Fitness and metrics
this.fitness = 0;
this.roadTime = 0; // Time spent on roads
this.convoyTime = 0; // Time spent in a convoy
this.parkingScore = 0; // Score for successful parking
this.trafficViolations = 0; // Count of lane violations, etc.
this.distanceTraveled = 0;
this.crashed = false;
this.timeAlive = epochTime * 0.8 + Math.random() * epochTime * 0.4; // Lifespan before attempting to park
// Sensors and visualization
this.sensors = Array(16).fill(0); // 16 general obstacle sensors
this.roadSensors = Array(8).fill(0); // Road-specific sensors
this.trafficSensors = Array(4).fill(0); // Traffic/convoy sensors
this.sensorRays = []; // Visual lines for sensors
this.flockLines = []; // Visual lines for convoy connections
this.neighbors = []; // Nearby cars for flocking/convoy logic
this.lastPosition = new THREE.Vector3(x, 1, z);
this.createSensorRays();
this.createFlockVisualization();
this.initializeMovement();
// Manual control inputs
this.manualAcceleration = 0;
this.manualBraking = 0;
this.manualSteer = 0; // -1 for left, 1 for right
}
createCarMesh() {
const group = new THREE.Group();
// Car body
const bodyGeometry = new THREE.BoxGeometry(1.5, 0.8, 3.5);
// Removed flatShading: true as it's not a property of MeshLambertMaterial
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 center y
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); // Front-left
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); // Front-right
group.add(this.rightSignal);
// Role indicator (cone above car)
const indicatorGeometry = new THREE.ConeGeometry(0.2, 0.8, 6);
this.roleIndicator = new THREE.Mesh(indicatorGeometry,
new THREE.MeshLambertMaterial({ color: 0xffffff })); // Default white
this.roleIndicator.position.set(0, 1.5, 0); // Above car body
group.add(this.roleIndicator);
// Wheels
const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 8); // radiusTop, radiusBottom, height, segments
const wheelMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
this.wheels = [];
const wheelPositions = [
[-0.7, 0, 1.4], [0.7, 0, 1.4], // Front wheels
[-0.7, 0, -1.4], [0.7, 0, -1.4] // Rear wheels
];
wheelPositions.forEach((pos) => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheel.position.set(...pos);
wheel.rotation.z = Math.PI / 2; // Rotate to lie flat
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++) { // 16 general obstacle sensors
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, 10) // Default length 10
]);
const ray = new THREE.Line(geometry, sensorMaterial);
this.sensorRays.push(ray);
this.mesh.add(ray); // Add rays as children of car mesh for relative positioning
}
}
createFlockVisualization() {
const flockMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.6,
linewidth: 2 // Note: linewidth might not be supported by all WebGL renderers
});
for (let i = 0; i < 10; i++) { // Max 10 flock lines per car
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 2, 0), // Start point (relative to world for now)
new THREE.Vector3(0, 2, 0) // End point
]);
const line = new THREE.Line(geometry, flockMaterial);
this.flockLines.push(line);
if (showFlockLines) scene.add(line); // Add to scene directly
}
}
initializeMovement() {
const nearestRoad = this.findNearestRoad();
if (nearestRoad) {
this.currentLane = nearestRoad.lane; // Store lane type (e.g., 'highway_horizontal')
this.mesh.rotation.y = nearestRoad.direction;
this.velocity.set(
Math.sin(nearestRoad.direction) * 8, 0, Math.cos(nearestRoad.direction) * 8
);
} else {
// Random initial orientation and velocity if no road found
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() {
// This function is complex and crucial. It determines the closest road segment.
// It considers different road types (highways, secondary, local, access)
// and returns information about the road (type, center, direction, width).
// For brevity, its detailed implementation is assumed from the original code,
// but it needs to be robust for multilane scenarios.
// Key is that it returns an object like:
// { lane: 'type_orientation', center: number, direction: angle, width: number }
const pos = this.mesh.position;
let nearestRoadInfo = null;
let minDistance = Infinity;
world.roads.forEach(road => {
let distanceToRoadCenterLine;
let roadCenterCoord; // x for vertical, z for horizontal
let carRelevantCoord; // x for vertical, z for horizontal
let roadWidth = road.width;
if (road.direction === 'horizontal') {
roadCenterCoord = road.z;
carRelevantCoord = pos.z;
// Check if car is within road's x-bounds
if (pos.x < road.start || pos.x > road.end) return;
} else { // vertical
roadCenterCoord = road.x;
carRelevantCoord = pos.x;
// Check if car is within road's z-bounds
if (pos.z < road.start || pos.z > road.end) return;
}
distanceToRoadCenterLine = Math.abs(carRelevantCoord - roadCenterCoord);
if (distanceToRoadCenterLine < roadWidth / 2 + 5) { // Consider roads slightly wider for detection
if (distanceToRoadCenterLine < minDistance) {
minDistance = distanceToRoadCenterLine;
nearestRoadInfo = {
lane: `${road.type}_${road.direction}`,
center: roadCenterCoord, // Centerline coordinate (x or z)
direction: road.orientationAngle, // Actual angle of the road
width: roadWidth,
roadObject: road // Reference to the road object itself
};
}
}
});
return nearestRoadInfo;
}
getRoadPositionScore() { // Renamed from getRoadPosition to avoid conflict
// Calculates a score (0-1) based on how well the car is on *any* road surface.
// Higher score means more centered on a road.
const pos = this.mesh.position;
let maxRoadScore = 0;
world.roads.forEach(road => {
let distanceToRoadCenterLine;
let carRelevantCoord;
if (road.direction === 'horizontal') {
if (pos.x < road.start || pos.x > road.end) return; // Outside road segment length
carRelevantCoord = pos.z;
distanceToRoadCenterLine = Math.abs(carRelevantCoord - road.z);
} else { // vertical
if (pos.z < road.start || pos.z > road.end) return; // Outside road segment length
carRelevantCoord = pos.x;
distanceToRoadCenterLine = Math.abs(carRelevantCoord - road.x);
}
if (distanceToRoadCenterLine <= road.width / 2) {
maxRoadScore = Math.max(maxRoadScore, 1 - (distanceToRoadCenterLine / (road.width / 2)));
}
});
return maxRoadScore;
}
updateSensors() {
const maxDistance = 10; // Max sensor range
const raycaster = new THREE.Raycaster();
// 16-direction obstacle sensors
for (let i = 0; i < 16; i++) {
const angle = (i * Math.PI * 2) / 16; // Angle for this sensor ray
const direction = new THREE.Vector3(Math.sin(angle), 0, Math.cos(angle));
direction.applyQuaternion(this.mesh.quaternion); // Rotate ray with car's orientation
raycaster.set(this.mesh.position, direction);
const intersects = raycaster.intersectObjects(this.getObstacles(), true); // Check against other cars and buildings
if (intersects.length > 0 && intersects[0].distance <= maxDistance) {
this.sensors[i] = 1 - (intersects[0].distance / maxDistance); // Normalized sensor reading (1 = close, 0 = far)
} else {
this.sensors[i] = 0; // No obstacle detected in range
}
// 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]); // Update ray geometry
}
this.updateRoadSensors();
this.updateTrafficSensors();
}
updateRoadSensors() {
// Simplified for now, these would provide detailed info about road layout, intersections, etc.
this.roadSensors[0] = this.getRoadPositionScore(); // How well on a road
this.roadSensors[1] = this.getLanePosition(); // Position within current lane
this.roadSensors[2] = this.getRoadDirectionAlignment(); // Alignment with road direction
this.roadSensors[3] = this.getDistanceToIntersection(); // Normalized distance to nearest intersection
this.roadSensors[4] = this.getNearestParkingLotProximity(); // Proximity to parking
this.roadSensors[5] = this.getParkingAvailability(); // Availability in target lot
this.roadSensors[6] = this.getTrafficDensity(); // Local traffic density
this.roadSensors[7] = this.getOptimalSpeedFactor(); // Factor based on road type/density
}
getLanePosition() {
const roadInfo = this.findNearestRoad();
if (!roadInfo || !roadInfo.roadObject) return 0.5; // Default to center if no road
const pos = this.mesh.position;
let laneOffset;
if (roadInfo.roadObject.direction === 'horizontal') {
laneOffset = pos.z - roadInfo.center;
} else { // vertical
laneOffset = pos.x - roadInfo.center;
}
// Normalize to -1 (left edge of road) to 1 (right edge of road)
let normalizedPosition = (laneOffset / (roadInfo.width / 2));
// Then map to 0-1 for NN input (0 = left edge, 0.5 = center, 1 = right edge)
return Math.max(0, Math.min(1, (normalizedPosition + 1) / 2));
}
getRoadDirectionAlignment() {
const roadInfo = this.findNearestRoad();
if (!roadInfo) return 0.5; // Neutral if no road
const carDirectionVector = new THREE.Vector3(0,0,1).applyQuaternion(this.mesh.quaternion);
const roadDirectionVector = new THREE.Vector3(Math.sin(roadInfo.direction), 0, Math.cos(roadInfo.direction));
const dotProduct = carDirectionVector.dot(roadDirectionVector); // Ranges from -1 to 1
return (dotProduct + 1) / 2; // Normalize to 0-1 (1 = perfectly aligned)
}
// Placeholder functions for other road sensors - these would need detailed implementation
getDistanceToIntersection() { return Math.random(); }
getNearestParkingLotProximity() {
if (!this.targetParkingLot) return 0;
const dist = this.mesh.position.distanceTo(this.targetParkingLot.center);
return Math.max(0, 1 - dist / 100); // Normalized proximity
}
getParkingAvailability() {
if (!this.targetParkingLot) return 0;
const availableSpots = this.targetParkingLot.spots.filter(spot => !spot.occupied).length;
return this.targetParkingLot.spots.length > 0 ? availableSpots / this.targetParkingLot.spots.length : 0;
}
getTrafficDensity() { return Math.random() * 0.5; } // Simplified
getOptimalSpeedFactor() { return 0.8 + Math.random() * 0.2; } // Simplified
updateTrafficSensors() {
// Sensors related to convoy behavior, following, parking needs
this.trafficSensors[0] = this.convoyPosition >= 0 ? 1 : 0; // In convoy?
this.trafficSensors[1] = this.followTarget ? Math.min(this.mesh.position.distanceTo(this.followTarget.mesh.position) / 20, 1) : 1; // Normalized follow distance
this.trafficSensors[2] = this.convoyLeader ? Math.max(0, 1 - this.mesh.position.distanceTo(this.convoyLeader.mesh.position) / 50) : 0; // Proximity to leader
this.trafficSensors[3] = (this.timeAlive < epochTime * 0.3 && !this.isParked) ? 1 : 0; // Need to park?
}
updateConvoyBehavior() {
// Complex logic for forming, joining, and maintaining convoys.
// Includes finding neighbors, determining roles (leader/follower),
// and setting follow targets.
// This is a substantial part of the AI's social behavior.
// For brevity, its detailed implementation is assumed from original,
// but it would interact with the new NN outputs and traits.
this.neighbors = [];
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);
}
});
this.updateRole(); // Determine if leader, driver, parker
this.updateConvoyFormation(); // Manage followers or follow leader
}
updateRole() {
const roadPosScore = this.getRoadPositionScore();
if (this.isParked || this.isParkingApproach || this.isInApproachLane || this.isInExitLane) {
this.role = 'parker';
} else if (roadPosScore > 0.8 && this.neighbors.length > 1 && this.brain.trafficTraits.convoyDiscipline > 0.6) {
this.role = 'leader';
} else {
this.role = 'driver';
}
// Update role indicator color
if (this.role === 'leader') this.roleIndicator.material.color.setHex(0xff00ff); // Magenta
else if (this.role === 'parker') this.roleIndicator.material.color.setHex(0x00ff00); // Green
else this.roleIndicator.material.color.setHex(0xffffff); // White
}
updateConvoyFormation() {
// Simplified: Leader tries to get followers, followers try to follow leader or car ahead.
if (this.role === 'leader') {
this.convoyFollowers = this.neighbors
.filter(car => car.role === 'driver' && !car.convoyLeader && car.brain.trafficTraits.convoyDiscipline > 0.5)
.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 + 1; // Leader is 0, followers start at 1
follower.followTarget = index === 0 ? this : this.convoyFollowers[index-1];
});
} else if (this.convoyLeader && (this.convoyLeader.crashed || !this.convoyLeader.convoyFollowers.includes(this))) {
// If leader is gone or no longer recognizes this car as follower
this.convoyLeader = null;
this.followTarget = null;
this.convoyPosition = -1;
}
}
getEnhancedInputs() {
return [
...this.sensors, // 16 obstacle sensors
...this.roadSensors, // 8 road/navigation sensors
...this.trafficSensors // 4 traffic behavior sensors
];
}
update(deltaTime) {
if (this.crashed) return;
// Handle manual control if active
if (this === manuallyControlledCar && cameraMode === 'follow_best') {
this.applyManualControls(deltaTime);
// Common updates even for manually controlled car
this.updateVisuals();
this.checkCollisions(); // Still check for collisions
this.keepInBounds();
this.lastPosition.copy(this.mesh.position);
return; // Skip AI decision if manually controlled
}
// Handle parked cars separately
if (this.isParked) {
this.handleParkedBehavior(deltaTime);
this.updateVisuals(); // Keep visuals updated even when parked
return;
}
this.timeAlive -= deltaTime;
if (this.timeAlive <= 0 && !this.isParkingApproach && this.parkingAttempts < this.maxParkingAttempts) {
this.attemptParking(); // Try to park if lifespan is up
}
this.updateSensors();
this.updateConvoyBehavior();
this.updateVisuals();
const inputs = this.getEnhancedInputs();
const outputs = this.brain.activate(inputs);
this.applyTrafficMovement(outputs, deltaTime);
this.updateFitness(deltaTime);
this.lastPosition.copy(this.mesh.position);
this.checkCollisions();
this.keepInBounds();
// Grass behavior
const currentRoadPosScore = this.getRoadPositionScore();
if (currentRoadPosScore < GRASS_THRESHOLD && !this.isReturningToRoad && !this.isParkingApproach && !this.isInApproachLane && !this.isInExitLane) {
this.isReturningToRoad = true;
this.turnAngleGoal = Math.PI; // 180 degrees
this.turnProgress = 0;
this.initialOrientationY = this.mesh.rotation.y;
}
this.lastRoadPositionScore = currentRoadPosScore;
}
applyManualControls(deltaTime) {
const moveSpeed = 20.0;
const turnSpeed = 1.5;
if (manualControls.W) {
this.velocity.add(new THREE.Vector3(0,0,1).applyQuaternion(this.mesh.quaternion).multiplyScalar(moveSpeed * deltaTime));
}
if (manualControls.S) {
this.velocity.sub(new THREE.Vector3(0,0,1).applyQuaternion(this.mesh.quaternion).multiplyScalar(moveSpeed * 0.7 * deltaTime));
}
if (!manualControls.W && !manualControls.S) {
this.velocity.multiplyScalar(0.95); // Friction
}
if (manualControls.A) {
this.mesh.rotation.y += turnSpeed * deltaTime;
}
if (manualControls.D) {
this.mesh.rotation.y -= turnSpeed * deltaTime;
}
// Clamp speed
const currentSpeed = this.velocity.length();
if (currentSpeed > this.maxSpeed) {
this.velocity.normalize().multiplyScalar(this.maxSpeed);
}
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
this.wheels.forEach(wheel => wheel.rotation.x += currentSpeed * deltaTime * 0.1);
}
handleParkedBehavior(deltaTime) {
this.velocity.set(0,0,0); // Ensure car is stationary
this.departureTime -= deltaTime;
if (this.departureTime <= 0 && !this.isExitingParking) {
this.isExitingParking = true;
if (this.targetParkingLot && this.targetParkingLot.building) {
this.targetParkingLot.building.visitorCount = Math.max(0, (this.targetParkingLot.building.visitorCount || 0) - 1);
}
this.turnAngleGoal = Math.PI; // 180 degrees to exit
this.turnProgress = 0;
this.initialOrientationY = this.mesh.rotation.y; // Store orientation before turning
}
if (this.isExitingParking) {
const turnSpeedForExit = Math.PI / 2; // Turn 180 in 2 seconds
this.mesh.rotation.y += turnSpeedForExit * deltaTime;
this.turnProgress += turnSpeedForExit * deltaTime;
if (this.turnProgress >= this.turnAngleGoal) {
this.mesh.rotation.y = this.initialOrientationY + Math.PI; // Ensure exact 180 turn
this.isExitingParking = false;
this.leaveParking(); // This will set it to use an exit lane
}
}
}
applyTrafficMovement(outputs, deltaTime) {
const [
acceleration, braking, steerLeft, steerRight,
laneChangeIntent, followConvoySignal, parkingManeuverSignal,
turnSignalLeftOutput, turnSignalRightOutput, emergencyStopSignal
] = outputs;
// Update turn signals
this.turnSignal = 'none';
if (turnSignalLeftOutput > 0.7) this.turnSignal = 'left';
if (turnSignalRightOutput > 0.7) this.turnSignal = 'right';
this.leftSignal.material.opacity = this.turnSignal === 'left' ? (Math.sin(Date.now()*0.01) * 0.4 + 0.6) : 0.3;
this.rightSignal.material.opacity = this.turnSignal === 'right' ? (Math.sin(Date.now()*0.01) * 0.4 + 0.6) : 0.3;
if (this.isReturningToRoad) {
const turnSpeedReturn = Math.PI / 1.5; // Faster turn for recovery
this.mesh.rotation.y += turnSpeedReturn * deltaTime;
this.turnProgress += turnSpeedReturn * deltaTime;
this.velocity.copy(new THREE.Vector3(0,0,1).applyQuaternion(this.mesh.quaternion).multiplyScalar(this.minSpeed * 0.8)); // Move slowly while turning
if (this.turnProgress >= this.turnAngleGoal) {
this.mesh.rotation.y = this.initialOrientationY + Math.PI; // Ensure exact turn
this.isReturningToRoad = false;
}
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
return; // Override other movements while returning to road
}
if (emergencyStopSignal > 0.8) {
this.velocity.multiplyScalar(0.7); return;
}
if (parkingManeuverSignal > 0.7 && !this.isParked && !this.isParkingApproach) {
this.attemptParking(); return;
}
if (this.isParkingApproach || this.isInApproachLane || this.isInExitLane) {
this.executeParkingLogic(deltaTime); return; // Dedicated parking movement
}
this.followRoad(deltaTime, laneChangeIntent); // Pass laneChangeIntent
if (followConvoySignal > 0.6 && this.followTarget) {
this.followConvoyTarget(deltaTime);
}
// Basic movement based on NN outputs
const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
if (acceleration > 0.3) {
this.velocity.add(forward.multiplyScalar(acceleration * 10 * deltaTime));
}
if (braking > 0.5) {
this.velocity.multiplyScalar(1 - braking * deltaTime * 4);
}
const steering = (steerRight - steerLeft) * 0.10 * deltaTime * (this.velocity.length()/this.maxSpeed + 0.2); // Speed sensitive steering
this.mesh.rotation.y += steering;
// Speed limits and friction
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);
else if (currentSpeed < 0.1) this.velocity.set(0,0,0);
this.velocity.multiplyScalar(0.99); // General friction
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
this.wheels.forEach(wheel => wheel.rotation.x += currentSpeed * deltaTime * 0.1);
}
followRoad(deltaTime, laneChangeIntent) {
const roadInfo = this.findNearestRoad();
if (!roadInfo || !roadInfo.roadObject) {
// If completely off-road, increase penalty or trigger recovery
this.fitness -= 2 * deltaTime; // Penalty for being off-road
return;
}
const road = roadInfo.roadObject;
const targetRoadAngle = road.orientationAngle;
let carAngle = this.mesh.rotation.y;
// Normalize carAngle to be in similar range as targetRoadAngle (0 to 2PI or -PI to PI)
// Assuming targetRoadAngle is between -PI and PI from atan2
while (carAngle - targetRoadAngle > Math.PI) carAngle -= 2 * Math.PI;
while (targetRoadAngle - carAngle > Math.PI) carAngle += 2 * Math.PI;
let angleDiff = targetRoadAngle - carAngle;
// Correct smallest angle
if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
if (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
// Steering correction to align with road
this.mesh.rotation.y += angleDiff * 0.1 * this.brain.trafficTraits.laneKeeping;
// Lane keeping: Aim for a specific lane within the road width
const numLanes = Math.max(1, Math.floor(road.width / ROAD_WIDTH_UNIT));
let targetLaneIndex = Math.floor(numLanes / 2); // Default to center-ish lane
// Interpret laneChangeIntent (0-1) - simplified
if (numLanes > 1) {
if (laneChangeIntent < 0.33) targetLaneIndex = Math.max(0, targetLaneIndex -1 ); // Try to move left
else if (laneChangeIntent > 0.66) targetLaneIndex = Math.min(numLanes - 1, targetLaneIndex + 1); // Try to move right
}
// Calculate the center of the target lane
let targetLaneCenterCoord; // This will be an X or Z coordinate
const laneCenterOffsetFromRoadEdge = (targetLaneIndex + 0.5) * ROAD_WIDTH_UNIT;
if (road.direction === 'horizontal') {
// For horizontal roads, lanes are offset in Z from the road's Z center.
// Road center is road.z. Road edge is road.z - road.width/2.
targetLaneCenterCoord = (road.z - road.width/2) + laneCenterOffsetFromRoadEdge;
const currentZ = this.mesh.position.z;
const offsetFromTargetLane = currentZ - targetLaneCenterCoord;
this.velocity.z -= offsetFromTargetLane * 0.2 * this.brain.trafficTraits.laneKeeping * deltaTime;
if (Math.abs(offsetFromTargetLane) > ROAD_WIDTH_UNIT / 2) { // Outside target lane
this.trafficViolations++; laneViolations++;
}
} else { // Vertical road
// For vertical roads, lanes are offset in X from the road's X center.
targetLaneCenterCoord = (road.x - road.width/2) + laneCenterOffsetFromRoadEdge;
const currentX = this.mesh.position.x;
const offsetFromTargetLane = currentX - targetLaneCenterCoord;
this.velocity.x -= offsetFromTargetLane * 0.2 * this.brain.trafficTraits.laneKeeping * deltaTime;
if (Math.abs(offsetFromTargetLane) > ROAD_WIDTH_UNIT / 2) {
this.trafficViolations++; laneViolations++;
}
}
this.roadTime += deltaTime;
this.laneDiscipline = Math.max(0, 1 - (this.trafficViolations / (this.roadTime + 1)) * 0.1);
}
followConvoyTarget(deltaTime) {
if (!this.followTarget || this.followTarget.crashed) {
this.convoyLeader = null; this.followTarget = null; this.convoyPosition = -1; return;
}
const targetPos = this.followTarget.mesh.position;
const distance = this.mesh.position.distanceTo(targetPos);
const idealDistance = FOLLOW_DISTANCE + (this.convoyPosition * 2.5); // Staggered formation
const directionToTarget = targetPos.clone().sub(this.mesh.position).normalize();
if (distance > idealDistance + 2) { // Too far, speed up
this.velocity.add(directionToTarget.multiplyScalar(this.brain.trafficTraits.followingBehavior * 5 * deltaTime));
} else if (distance < idealDistance - 1) { // Too close, slow down
this.velocity.multiplyScalar(1 - (1 - this.brain.trafficTraits.followingBehavior) * 0.5 * deltaTime);
}
// Align with target's general direction (simplified)
const targetAngle = Math.atan2(directionToTarget.x, directionToTarget.z);
let carAngle = this.mesh.rotation.y;
while (carAngle - targetAngle > Math.PI) carAngle -= 2 * Math.PI;
while (targetAngle - carAngle > Math.PI) carAngle += 2 * Math.PI;
this.mesh.rotation.y += (targetAngle - carAngle) * 0.05;
this.convoyTime += deltaTime;
this.followingDistance = distance;
}
executeParkingLogic(deltaTime) {
// This is the state machine for parking
if (!this.targetParkingLot) { this.isParkingApproach = false; return; }
if (this.isInExitLane) {
this.handleExitLane(deltaTime);
} else if (this.isInApproachLane) {
this.handleApproachLaneMovement(deltaTime);
} else if (this.isParkingApproach) { // Moving towards an approach lane or spot
this.moveTowardsParkingEntry(deltaTime);
}
}
moveTowardsParkingEntry(deltaTime) {
// Try to enter an approach lane first
if (!this.targetParkingLot.approachLanes || this.targetParkingLot.approachLanes.length === 0) {
this.isParkingApproach = false; return; // No approach lanes defined
}
// Find the best (e.g. least occupied or closest) approach lane entry point
let bestLaneEntry = null;
let minOccupancy = Infinity; // Or some other metric like distance
let selectedLaneIdx = -1;
this.targetParkingLot.approachLanes.forEach((laneQueuePositions, idx) => {
// Simplified: pick the first available slot in any lane's queue start
// A more complex logic would check occupancy or distance.
if (laneQueuePositions.length > 0) {
// Check if first spot in this lane queue is free enough
const entryPoint = laneQueuePositions[0];
const occupied = population.some(car => car !== this && car.isInApproachLane && car.selectedApproachLaneIndex === idx && car.mesh.position.distanceTo(entryPoint) < 5);
if (!occupied && !bestLaneEntry) { // Simple: take first available
bestLaneEntry = entryPoint;
selectedLaneIdx = idx;
}
}
});
if (bestLaneEntry) {
this.approachTargetPosition = bestLaneEntry;
this.selectedApproachLaneIndex = selectedLaneIdx;
this.moveToPosition(this.approachTargetPosition, deltaTime, 3); // Slow approach speed
if (this.mesh.position.distanceTo(this.approachTargetPosition) < 2) {
this.isInApproachLane = true; // Entered the approach lane
this.isParkingApproach = false; // No longer just "approaching", now "in lane"
// Add to parking lot's internal queue for this lane if it has one
}
} else {
// All approach lanes seem full or no entry point found, wait or give up
this.velocity.multiplyScalar(0.9); // Slow down if can't find entry
this.parkingAttempts++;
if(this.parkingAttempts >= this.maxParkingAttempts) this.isParkingApproach = false;
}
}
handleApproachLaneMovement(deltaTime) {
// Logic for moving within the approach lane and finding a spot
if (!this.targetParkingLot || !this.targetParkingLot.spots) {
this.isInApproachLane = false; return;
}
const availableSpot = this.targetParkingLot.spots.find(spot => !spot.occupied);
if (availableSpot) {
this.moveToPosition(availableSpot.position, deltaTime, 2); // Move to spot
if (this.mesh.position.distanceTo(availableSpot.position) < 1.5) {
this.completeParkingProcess(availableSpot);
}
} else {
// No spot, wait in approach lane (simplified: just slow down)
this.velocity.multiplyScalar(0.95);
// Potentially move along queue if implemented
}
}
completeParkingProcess(spot) {
this.isParked = true;
this.parkingSpot = spot;
spot.occupied = true;
spot.car = this;
this.mesh.position.copy(spot.position);
this.mesh.rotation.y = spot.orientation !== undefined ? spot.orientation : this.mesh.rotation.y; // Align with spot
this.velocity.set(0, 0, 0);
this.parkingScore += 100;
parkingEvents++;
this.isInApproachLane = false;
this.isParkingApproach = false;
this.departureTime = 15 + Math.random() * 5; // Park for 15-20 seconds
if (this.targetParkingLot && this.targetParkingLot.building) {
this.targetParkingLot.building.visitorCount = (this.targetParkingLot.building.visitorCount || 0) + 1;
}
this.updateCarColor();
}
handleExitLane(deltaTime) {
if (!this.exitTargetPosition) { // Should have been set by leaveParking
this.isInExitLane = false;
this.role = 'driver';
this.timeAlive = epochTime * 0.5; // Give some time to drive away
return;
}
this.moveToPosition(this.exitTargetPosition, deltaTime, 4); // Move along exit lane
if (this.mesh.position.distanceTo(this.exitTargetPosition) < 2) {
// Reached end of exit lane segment, transition to road
this.isInExitLane = false;
this.role = 'driver';
this.timeAlive = epochTime * 0.7; // Replenish some time
this.initializeMovement(); // Re-orient and set velocity for road
this.updateCarColor();
}
}
leaveParking() {
if (!this.isParked && !this.isInExitLane) return; // Not parked or already exiting
if (this.parkingSpot) {
this.parkingSpot.occupied = false;
this.parkingSpot.car = null;
this.parkingSpot = null;
}
this.isParked = false;
// Find an exit lane target
if (this.targetParkingLot && this.targetParkingLot.exitLanes && this.targetParkingLot.exitLanes.length > 0) {
// Simplified: pick first exit lane, last point as target
// A real system would pick closest/least congested
this.selectedExitLaneIndex = 0; // Or a smarter choice
const exitLanePoints = this.targetParkingLot.exitLanes[this.selectedExitLaneIndex];
if (exitLanePoints && exitLanePoints.length > 0) {
this.exitTargetPosition = exitLanePoints[exitLanePoints.length - 1]; // Target the end of the exit lane
this.isInExitLane = true;
this.isExitingParking = false; // Done with 180 turn
// Initial velocity towards exitTargetPosition will be handled by moveToPosition
} else {
this.role = 'driver'; // Fallback if exit lane is weird
this.initializeMovement();
}
} else {
this.role = 'driver'; // Fallback if no exit lanes
this.initializeMovement();
}
this.updateCarColor();
}
attemptParking() {
if (this.isParked || this.isParkingApproach) return;
this.role = 'parker';
this.updateRole(); // Update indicator
this.findNearestParkingLotForAI(); // Sets this.targetParkingLot
if (!this.targetParkingLot) {
this.parkingAttempts++; // Failed to find a lot
this.role = 'driver'; this.updateRole();
this.timeAlive = epochTime * 0.2; // Try again sooner
return;
}
this.isParkingApproach = true; // Start the approach process
this.parkingAttempts = 0; // Reset attempts for this lot
}
findNearestParkingLotForAI() {
let closestLot = null;
let minDist = Infinity;
world.parkingLots.forEach(lot => {
const dist = this.mesh.position.distanceTo(lot.center);
if (dist < minDist) {
minDist = dist;
closestLot = lot;
}
});
this.targetParkingLot = closestLot;
}
moveToPosition(targetPos, deltaTime, speed) {
const direction = targetPos.clone().sub(this.mesh.position);
const distance = direction.length();
if (distance > 0.5) { // Threshold to stop jittering
direction.normalize();
this.velocity.copy(direction.multiplyScalar(speed));
// Smoothly turn towards target
const targetAngle = Math.atan2(direction.x, direction.z);
let currentAngle = this.mesh.rotation.y;
// Normalize angles to prevent full circle turns
while (targetAngle - currentAngle > Math.PI) currentAngle += 2 * Math.PI;
while (currentAngle - targetAngle > Math.PI) currentAngle -= 2 * Math.PI;
this.mesh.rotation.y += (targetAngle - currentAngle) * 0.2; // Adjust turn speed
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
} else {
this.velocity.set(0,0,0); // Reached target
}
}
updateFitness(deltaTime) {
const distance = this.mesh.position.distanceTo(this.lastPosition);
this.distanceTraveled += distance;
let fitnessScore = this.distanceTraveled * 0.5; // Base score for moving
fitnessScore += this.roadTime * 1.5; // Bonus for staying on road
fitnessScore += this.convoyTime * 1.0; // Bonus for being in convoy
fitnessScore += this.parkingScore * 0.5; // Bonus for parking successfully
fitnessScore -= this.trafficViolations * 5; // Penalty for violations
if (this.getRoadPositionScore() < GRASS_THRESHOLD && !this.isReturningToRoad) {
fitnessScore -= 10 * deltaTime; // Heavy penalty for being on grass without trying to return
}
if (this.crashed) fitnessScore -= 500; // Large penalty for crashing
this.fitness = fitnessScore;
}
updateVisuals() {
this.updateCarColor();
this.updateFlockVisualization();
this.updateRole(); // Ensure role indicator is current
}
updateCarColor() {
let hue = 0.6, saturation = 0.7, lightness = 0.5; // Default blue
if (this.isParked) { hue = 0.33; lightness = 0.7; } // Green
else if (this.role === 'leader') { hue = 0.83; saturation = 1.0; lightness = 0.6; } // Purple
else if (this.convoyPosition > 0) { hue = 0.5; saturation = 0.8; lightness = 0.6; } // Cyan
else if (this.getRoadPositionScore() < GRASS_THRESHOLD) { hue = 0.1; saturation = 1.0; } // Orange for off-road
const performanceBonus = Math.min(Math.max(0, this.fitness) / 1000, 0.2); // Brighter for higher fitness
lightness = Math.min(1, lightness + performanceBonus);
this.bodyMaterial.color.setHSL(hue, saturation, lightness);
}
updateFlockVisualization() {
// Manages lines connecting convoy members or nearby cars.
// Assumed from original, ensures lines are updated or hidden based on showFlockLines.
let lineIdx = 0;
if (showFlockLines) {
if (this.role === 'leader' && this.convoyFollowers) {
this.convoyFollowers.forEach(follower => {
if (lineIdx < this.flockLines.length && follower) {
this.flockLines[lineIdx].geometry.setFromPoints([this.mesh.position, follower.mesh.position]);
this.flockLines[lineIdx].material.color.setHex(0xff00ff); // Leader connections
this.flockLines[lineIdx].visible = true;
lineIdx++;
}
});
} else if (this.followTarget) {
if (lineIdx < this.flockLines.length) {
this.flockLines[lineIdx].geometry.setFromPoints([this.mesh.position, this.followTarget.mesh.position]);
this.flockLines[lineIdx].material.color.setHex(0x00ffff); // Follower connection
this.flockLines[lineIdx].visible = true;
lineIdx++;
}
}
}
for (let i = lineIdx; i < this.flockLines.length; i++) {
this.flockLines[i].visible = false; // Hide unused lines
}
}
getObstacles() {
// Returns an array of meshes that act as obstacles (other cars, buildings).
let obstacles = [];
population.forEach(car => {
if (car !== this && !car.crashed) obstacles.push(car.mesh);
});
world.buildings.forEach(buildingData => obstacles.push(buildingData.mesh));
return obstacles;
}
checkCollisions() {
if (this.crashed) return;
const carBox = new THREE.Box3().setFromObject(this.mesh);
// Car-to-car collisions (soft)
population.forEach(otherCar => {
if (otherCar !== this && !otherCar.crashed) {
const otherBox = new THREE.Box3().setFromObject(otherCar.mesh);
if (carBox.intersectsBox(otherBox)) {
// Soft collision: push apart slightly, reduce fitness
const separationVector = this.mesh.position.clone().sub(otherCar.mesh.position).normalize().multiplyScalar(0.2);
this.mesh.position.add(separationVector);
otherCar.mesh.position.sub(separationVector);
this.velocity.multiplyScalar(0.8); otherCar.velocity.multiplyScalar(0.8);
this.fitness -= 5; otherCar.fitness -= 5;
this.trafficViolations++; otherCar.trafficViolations++;
// Minor chance of full crash from soft collision
if (Math.random() < 0.01 && !this.isParkingRelatedState() && !otherCar.isParkingRelatedState()) {
this.crashed = true; crashCount++;
}
}
}
});
// Car-to-building collisions (hard)
world.buildings.forEach(buildingData => {
const buildingBox = new THREE.Box3().setFromObject(buildingData.mesh);
if (carBox.intersectsBox(buildingBox)) {
this.crashed = true; crashCount++;
}
});
}
isParkingRelatedState() {
return this.isParked || this.isParkingApproach || this.isInApproachLane || this.isInExitLane;
}
keepInBounds() {
const bounds = 400; // World boundary
if (Math.abs(this.mesh.position.x) > bounds || Math.abs(this.mesh.position.z) > bounds) {
this.mesh.position.x = Math.max(-bounds, Math.min(bounds, this.mesh.position.x));
this.mesh.position.z = Math.max(-bounds, Math.min(bounds, this.mesh.position.z));
this.velocity.multiplyScalar(-0.5); // Bounce back
this.fitness -= 20; // Penalty for hitting boundary
}
}
destroy() {
// Clean up Three.js objects and any references
if (this.parkingSpot) {
this.parkingSpot.occupied = false; this.parkingSpot.car = null;
}
if (this.targetParkingLot && this.targetParkingLot.building && this.isParked) { // Only decrement if it was parked and is now destroyed
this.targetParkingLot.building.visitorCount = Math.max(0, (this.targetParkingLot.building.visitorCount || 0) - 1);
}
this.flockLines.forEach(line => { if (line.parent) scene.remove(line); });
if (this.mesh.parent) scene.remove(this.mesh);
}
}
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB); // Sky blue
scene.fog = new THREE.Fog(0x87CEEB, 300, 1000); // Fog for depth effect
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; // Softer shadows
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0x606060); // Increased ambient light
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(100, 150, 75); // Adjusted light angle
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048; // Higher shadow resolution
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 50;
directionalLight.shadow.camera.far = 500;
directionalLight.shadow.camera.left = -200;
directionalLight.shadow.camera.right = 200;
directionalLight.shadow.camera.top = 200;
directionalLight.shadow.camera.bottom = -200;
scene.add(directionalLight);
createTrafficWorld();
createInitialPopulation();
clock = new THREE.Clock();
window.addEventListener('resize', onWindowResize);
setupEventListeners();
animate();
}
function createTrafficWorld() {
// Ground plane
const groundGeometry = new THREE.PlaneGeometry(1200, 1200);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x3c763d }); // Darker green
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = 0; // Ground at y=0
ground.receiveShadow = true;
scene.add(ground);
createRoadNetwork(); // Roads first
createBuildingsWithParkingLots(); // Then buildings and their parking
}
function createRoad(x, z, width, length, type, orientationAngle, isHorizontal) {
const roadHeight = 0.1; // Roads slightly above ground
const roadMaterial = new THREE.MeshLambertMaterial({ color: type === 'highway' ? 0x333333 : 0x444444 });
const roadGeometry = new THREE.PlaneGeometry(isHorizontal ? length : width, isHorizontal ? width : length);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
roadMesh.position.set(x, roadHeight, z);
roadMesh.receiveShadow = true;
scene.add(roadMesh);
const roadData = {
mesh: roadMesh,
x: x, z: z, // Center of the road segment
width: width, length: length,
type: type,
direction: isHorizontal ? 'horizontal' : 'vertical',
orientationAngle: orientationAngle, // Angle in radians
start: isHorizontal ? x - length/2 : z - length/2, // Start coord for segment bounds
end: isHorizontal ? x + length/2 : z + length/2, // End coord for segment bounds
lanes: [] // Store lane data if needed later
};
world.roads.push(roadData);
// Lane markings
const numLanes = Math.max(1, Math.floor(width / ROAD_WIDTH_UNIT));
const actualLaneWidth = width / numLanes;
const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const yellowLineMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
for (let i = 0; i < numLanes; i++) {
// Calculate offset for this lane's center from the road's center line
const laneCenterOffset = (i - (numLanes - 1) / 2) * actualLaneWidth;
// Add dashed lines between lanes (if not the outermost edge)
if (i < numLanes - 1) {
let linePosX, linePosZ, lineWidth, lineHeight;
const lineOffsetFromLaneCenter = actualLaneWidth / 2; // Line is at the edge of the lane
if (isHorizontal) {
linePosX = x; // Centered with road segment
linePosZ = z + laneCenterOffset + lineOffsetFromLaneCenter;
lineWidth = length;
lineHeight = 0.2;
} else { // Vertical
linePosX = x + laneCenterOffset + lineOffsetFromLaneCenter;
linePosZ = z; // Centered with road segment
lineWidth = 0.2;
lineHeight = length;
}
// Use yellow for center divider on multi-lane roads (simplified: if it's near overall center)
const isCenterDivider = numLanes > 1 && Math.abs(laneCenterOffset + lineOffsetFromLaneCenter) < actualLaneWidth * 0.6;
createDashedLineWorld(linePosX, linePosZ, lineWidth, lineHeight, isHorizontal, isCenterDivider ? yellowLineMaterial : lineMaterial, roadHeight + 0.01);
}
}
}
function createDashedLineWorld(centerX, centerZ, totalLength, totalWidth, isHorizontal, material, yPos) {
const dashLength = 5;
const gapLength = 3;
const numDashes = Math.floor(totalLength / (dashLength + gapLength));
for (let j = 0; j < numDashes; j++) {
const dashGeometry = new THREE.PlaneGeometry(
isHorizontal ? dashLength : totalWidth,
isHorizontal ? totalWidth : dashLength
);
const dash = new THREE.Mesh(dashGeometry, material);
dash.rotation.x = -Math.PI / 2;
const dashOffset = j * (dashLength + gapLength) - totalLength / 2 + dashLength / 2;
if (isHorizontal) {
dash.position.set(centerX + dashOffset, yPos, centerZ);
} else {
dash.position.set(centerX, yPos, centerZ + dashOffset);
}
scene.add(dash);
}
}
function createRoadNetwork() {
world.roads = []; // Clear existing roads
// Main highways (e.g., 4 lanes wide = 24 units)
createRoad(0, 0, ROAD_WIDTH_UNIT * 4, 800, 'highway', 0, true); // Horizontal E-W
createRoad(0, 0, ROAD_WIDTH_UNIT * 4, 800, 'highway', Math.PI / 2, false); // Vertical N-S
// Secondary roads (e.g., 2 lanes wide = 12 units)
createRoad(0, ROAD_SPACING, ROAD_WIDTH_UNIT * 2, 800, 'secondary', 0, true);
createRoad(0, -ROAD_SPACING, ROAD_WIDTH_UNIT * 2, 800, 'secondary', 0, true);
createRoad(ROAD_SPACING, 0, ROAD_WIDTH_UNIT * 2, 800, 'secondary', Math.PI / 2, false);
createRoad(-ROAD_SPACING, 0, ROAD_WIDTH_UNIT * 2, 800, 'secondary', Math.PI / 2, false);
// More roads for a denser network
createRoad(0, ROAD_SPACING * 2, ROAD_WIDTH_UNIT * 2, 800, 'local', 0, true);
createRoad(0, -ROAD_SPACING * 2, ROAD_WIDTH_UNIT * 2, 800, 'local', 0, true);
createRoad(ROAD_SPACING * 2, 0, ROAD_WIDTH_UNIT * 2, 800, 'local', Math.PI / 2, false);
createRoad(-ROAD_SPACING * 2, 0, ROAD_WIDTH_UNIT * 2, 800, 'local', Math.PI / 2, false);
}
function createBuildingsWithParkingLots() {
world.buildings = []; world.parkingLots = []; // Clear previous
const buildingBaseMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa });
const parkingMaterial = new THREE.MeshLambertMaterial({ color: 0x383838 });
const spotMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 });
const barGraphMaterial = new THREE.MeshLambertMaterial({color: 0x007bff});
const buildingLocations = [
{ x: -100, z: -100 }, { x: 100, z: -100 },
{ x: -100, z: 100 }, { x: 100, z: 100 },
{ x: -250, z: -50 }, { x: 250, z: 50 },
{ x: -50, z: -250 }, { x: 50, z: 250 },
];
buildingLocations.forEach((loc, index) => {
const bWidth = 20 + Math.random() * 15;
const bHeight = 15 + Math.random() * 25;
const bDepth = 20 + Math.random() * 15;
const buildingGeometry = new THREE.BoxGeometry(bWidth, bHeight, bDepth);
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingBaseMaterial.clone());
buildingMesh.material.color.setHSL(Math.random(), 0.5, 0.6);
buildingMesh.position.set(loc.x, bHeight / 2 + 0.1, loc.z); // Slightly above ground
buildingMesh.castShadow = true;
scene.add(buildingMesh);
// Bar graph for visitor count
const barGeometry = new THREE.BoxGeometry(5, 1, 5); // Base size
const barGraphMesh = new THREE.Mesh(barGeometry, barGraphMaterial.clone());
barGraphMesh.position.set(loc.x, bHeight + 0.1 + 3, loc.z); // Position above building
barGraphMesh.scale.y = 0.1; // Start very small
barGraphMesh.visible = true;
scene.add(barGraphMesh);
const buildingData = { mesh: buildingMesh, parkingLot: null, visitorCount: 0, barGraphMesh: barGraphMesh, height: bHeight };
world.buildings.push(buildingData);
// Create parking lot next to building
const lotWidth = 40, lotDepth = 30;
const lotCenterX = loc.x + bWidth / 2 + lotWidth / 2 + 5; // East of building
const lotCenterZ = loc.z;
const lotGeometry = new THREE.PlaneGeometry(lotWidth, lotDepth);
const lotMesh = new THREE.Mesh(lotGeometry, parkingMaterial);
lotMesh.rotation.x = -Math.PI / 2;
lotMesh.position.set(lotCenterX, 0.05, lotCenterZ); // Slightly above ground, below roads
scene.add(lotMesh);
const parkingLot = {
center: new THREE.Vector3(lotCenterX, 0.1, lotCenterZ),
spots: [],
approachLanes: [], exitLanes: [], accessPoints: [], // For future advanced queueing
building: buildingData // Link back to building
};
buildingData.parkingLot = parkingLot; // Link building to its lot
// Parking spots (2 rows of 5)
const numRows = 2, spotsPerRow = 5;
for (let r = 0; r < numRows; r++) {
for (let s = 0; s < spotsPerRow; s++) {
const spotX = lotCenterX + (s - (spotsPerRow - 1)/2) * (PARKING_SPOT_SIZE.width + 2);
const spotZ = lotCenterZ + (r - (numRows - 1)/2) * (PARKING_SPOT_SIZE.length + 3);
const spotOrientation = Math.PI / 2; // Assuming spots are perpendicular to building side
const spotPlaneGeom = new THREE.PlaneGeometry(PARKING_SPOT_SIZE.width, PARKING_SPOT_SIZE.length);
const spotPlaneMesh = new THREE.Mesh(spotPlaneGeom, spotMaterial);
spotPlaneMesh.rotation.x = -Math.PI/2;
spotPlaneMesh.rotation.z = spotOrientation; // Align with how car would park
spotPlaneMesh.position.set(spotX, 0.06, spotZ);
scene.add(spotPlaneMesh);
parkingLot.spots.push({
position: new THREE.Vector3(spotX, 1, spotZ), // Car's y position when parked
orientation: spotOrientation, // Car's y rotation when parked
occupied: false, car: null, mesh: spotPlaneMesh
});
}
}
// Simplified approach/exit points for now
parkingLot.approachLanes.push([new THREE.Vector3(lotCenterX - lotWidth/2 - 5, 1, lotCenterZ)]); // Entry point
parkingLot.exitLanes.push([new THREE.Vector3(lotCenterX - lotWidth/2 - 10, 1, lotCenterZ + 5)]); // Exit point nearby
world.parkingLots.push(parkingLot);
});
}
function createInitialPopulation() {
population = [];
const startPositions = [ // Disperse starting positions
{x: -50, z: 0}, {x: 50, z: 0}, {x: 0, z: -50}, {x: 0, z: 50},
{x: -ROAD_SPACING, z: 0}, {x: ROAD_SPACING, z: 0},
{x: 0, z: -ROAD_SPACING}, {x: 0, z: ROAD_SPACING},
];
for (let i = 0; i < populationSize; i++) {
const pos = startPositions[i % startPositions.length];
const car = new TrafficCar(pos.x + Math.random()*10-5, pos.z + Math.random()*10-5);
population.push(car);
scene.add(car.mesh);
}
}
function evolvePopulation() {
population.sort((a, b) => (b.fitness || 0) - (a.fitness || 0)); // Higher fitness first
bestFitness = population[0] ? population[0].fitness : 0;
const eliteCount = Math.floor(populationSize * 0.1); // Top 10% survive
const survivors = population.slice(0, eliteCount);
const newPopulation = [];
// Add elites directly
survivors.forEach(parent => {
const offspring = new TrafficCar(parent.mesh.position.x, parent.mesh.position.z);
offspring.brain = parent.brain.copy(); // Elites pass genes directly
newPopulation.push(offspring);
});
// Fill rest with mutated offspring from survivors
while (newPopulation.length < populationSize) {
const parent = survivors[Math.floor(Math.random() * survivors.length)];
const offspring = new TrafficCar(parent.mesh.position.x, parent.mesh.position.z); // Start near parent
offspring.brain = parent.brain.copy();
offspring.brain.mutate(0.1); // Standard mutation rate
newPopulation.push(offspring);
}
// Cleanup old population and add new
population.forEach(car => car.destroy());
population = newPopulation;
population.forEach(car => scene.add(car.mesh));
epoch++;
timeLeft = epochTime;
crashCount = 0; parkingEvents = 0; laneViolations = 0;
world.parkingLots.forEach(lot => { // Reset parking lot visitor counts
if (lot.building) lot.building.visitorCount = 0;
lot.spots.forEach(spot => { spot.occupied = false; spot.car = null; });
});
console.log(`Epoch ${epoch}: Best Fitness: ${bestFitness.toFixed(1)}`);
}
function animate() {
requestAnimationFrame(animate);
const deltaTime = Math.min(clock.getDelta() * speedMultiplier, 0.1); // Cap delta
if (!paused) {
timeLeft -= deltaTime;
if (timeLeft <= 0) {
evolvePopulation();
}
updatePopulation(deltaTime);
updateCamera();
updateUI();
}
renderer.render(scene, camera);
}
function updatePopulation(deltaTime) {
let currentStats = { alive: 0, leaders: 0, convoy: 0, parked: 0, solo: 0, maxConvoySize: 0, totalRoadTime: 0, totalViolations: 0, totalFollowingDistance: 0, followingCount: 0, approaching:0 };
population.forEach(car => {
if (!car.crashed) {
car.update(deltaTime); // Car's internal update
currentStats.alive++;
if (car.isParked) currentStats.parked++;
else if (car.isParkingApproach || car.isInApproachLane) currentStats.approaching++;
else if (car.role === 'leader') currentStats.leaders++;
else if (car.convoyPosition > 0) {
currentStats.convoy++;
if (car.followTarget) {
currentStats.totalFollowingDistance += car.mesh.position.distanceTo(car.followTarget.mesh.position);
currentStats.followingCount++;
}
} else currentStats.solo++;
if (car.role==='leader' && car.convoyFollowers) currentStats.maxConvoySize = Math.max(currentStats.maxConvoySize, car.convoyFollowers.length + 1);
currentStats.totalRoadTime += car.roadTime;
currentStats.totalViolations += car.trafficViolations;
}
});
window.populationStats = currentStats; // Make accessible for UI
}
function updateCamera() {
let targetCar = null;
if (cameraMode === 'follow_best') {
targetCar = population.filter(c => !c.crashed && !c.isParked).sort((a,b) => b.fitness - a.fitness)[0];
manuallyControlledCar = targetCar; // Set for manual control
} else if (cameraMode === 'follow_convoy') {
targetCar = population.filter(c => c.role === 'leader' && c.convoyFollowers.length > 0)
.sort((a,b) => b.convoyFollowers.length - a.convoyFollowers.length)[0];
manuallyControlledCar = null;
} else {
manuallyControlledCar = null;
}
if (targetCar) {
const offset = new THREE.Vector3(0, 30, -25); // Higher and behind
const targetPosition = targetCar.mesh.position.clone().add(offset.applyQuaternion(targetCar.mesh.quaternion));
camera.position.lerp(targetPosition, 0.05);
camera.lookAt(targetCar.mesh.position);
} else { // Overview
camera.position.lerp(new THREE.Vector3(0, 200, 200), 0.02);
camera.lookAt(0, 0, 0);
}
}
function updateUI() {
const stats = window.populationStats || {};
document.getElementById('epoch').textContent = epoch;
document.getElementById('epochTime').textContent = Math.ceil(timeLeft);
document.getElementById('timeProgress').style.width = `${((epochTime - timeLeft) / epochTime) * 100}%`;
document.getElementById('population').textContent = stats.alive || 0;
document.getElementById('bestFitness').textContent = Math.round(bestFitness);
document.getElementById('trafficIQ').textContent = Math.round(50 + (bestFitness / 50)); // Scaled IQ
document.getElementById('roadMastery').textContent = stats.alive > 0 ? Math.round((stats.totalRoadTime / stats.alive) / epochTime * 100) : 0;
document.getElementById('crashCount').textContent = crashCount;
document.getElementById('parkingEvents').textContent = parkingEvents; // Global counter
document.getElementById('laneViolations').textContent = laneViolations; // Global counter
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;
// Update building bar graphs
world.buildings.forEach(buildingData => {
if (buildingData.barGraphMesh) {
const scaleY = Math.max(0.1, buildingData.visitorCount * 2); // Scale factor for bar height
buildingData.barGraphMesh.scale.y = scaleY;
// Adjust y position so it grows upwards from its base
buildingData.barGraphMesh.position.y = buildingData.height + 0.1 + 3 + (scaleY / 2) * buildingData.barGraphMesh.geometry.parameters.height;
}
});
updateTopPerformersDisplay(); // Separate function for clarity
}
function updateTopPerformersDisplay() {
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');
div.innerHTML = `${i + 1}. F:${Math.round(car.fitness)} Role:${car.role}`;
topPerformersDiv.appendChild(div);
});
}
function setupEventListeners() {
document.getElementById('pauseBtn').addEventListener('click', () => { paused = !paused; document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause'; });
document.getElementById('resetBtn').addEventListener('click', resetSimulation);
document.getElementById('speedBtn').addEventListener('click', () => { speedMultiplier = speedMultiplier === 1 ? 2 : speedMultiplier === 2 ? 5 : 1; document.getElementById('speedBtn').textContent = `Speed: ${speedMultiplier}x`; });
document.getElementById('viewBtn').addEventListener('click', () => {
const modes = ['overview', 'follow_best', 'follow_convoy'];
cameraMode = modes[(modes.indexOf(cameraMode) + 1) % modes.length];
document.getElementById('viewBtn').textContent = `View: ${cameraMode.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}`;
});
document.getElementById('flockBtn').addEventListener('click', () => {
showFlockLines = !showFlockLines;
document.getElementById('flockBtn').textContent = `Networks: ${showFlockLines ? 'ON' : 'OFF'}`;
world.flockLines.forEach(line => line.visible = showFlockLines); // Global flock lines (if any)
population.forEach(car => car.flockLines.forEach(line => line.visible = showFlockLines && (line.parent === scene))); // Car-specific lines
});
document.getElementById('trafficBtn').addEventListener('click', () => { /* trafficRules toggle, might affect AI behavior if implemented */ });
// Manual control listeners
document.addEventListener('keydown', (event) => {
if (manuallyControlledCar && cameraMode === 'follow_best') {
if (event.key === 'w' || event.key === 'W') manualControls.W = true;
if (event.key === 's' || event.key === 'S') manualControls.S = true;
if (event.key === 'a' || event.key === 'A') manualControls.A = true;
if (event.key === 'd' || event.key === 'D') manualControls.D = true;
}
});
document.addEventListener('keyup', (event) => {
if (manuallyControlledCar && cameraMode === 'follow_best') {
if (event.key === 'w' || event.key === 'W') manualControls.W = false;
if (event.key === 's' || event.key === 'S') manualControls.S = false;
if (event.key === 'a' || event.key === 'A') manualControls.A = false;
if (event.key === 'd' || event.key === 'D') manualControls.D = false;
}
});
}
function resetSimulation() {
epoch = 1; timeLeft = epochTime; bestFitness = 0; crashCount = 0; parkingEvents = 0; laneViolations = 0;
population.forEach(car => car.destroy()); // Proper cleanup
// Clear building visitor counts and bar graphs
world.buildings.forEach(buildingData => {
buildingData.visitorCount = 0;
if (buildingData.barGraphMesh) {
buildingData.barGraphMesh.scale.y = 0.1;
buildingData.barGraphMesh.position.y = buildingData.height + 0.1 + 3 + (0.1 / 2) * buildingData.barGraphMesh.geometry.parameters.height;
}
});
world.parkingLots.forEach(lot => {
lot.spots.forEach(spot => { spot.occupied = false; spot.car = null; });
});
createInitialPopulation();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
init();
</script>
</body>
</html>