Spaces:
Running
Running
<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> | |