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: 200px; | |
} | |
#controls { | |
position: absolute; | |
top: 10px; | |
right: 10px; | |
color: white; | |
background-color: rgba(0,0,0,0.9); | |
padding: 15px; | |
border-radius: 8px; | |
z-index: 100; | |
} | |
button { | |
background-color: #4CAF50; | |
border: none; | |
color: white; | |
padding: 8px 16px; | |
margin: 5px; | |
cursor: pointer; | |
border-radius: 4px; | |
font-size: 12px; | |
} | |
button:hover { | |
background-color: #45a049; | |
} | |
#stats { | |
position: absolute; | |
bottom: 10px; | |
left: 10px; | |
color: white; | |
background-color: rgba(0,0,0,0.9); | |
padding: 15px; | |
border-radius: 8px; | |
z-index: 100; | |
font-size: 12px; | |
min-width: 200px; | |
} | |
#flockingStats { | |
position: absolute; | |
bottom: 10px; | |
right: 10px; | |
color: white; | |
background-color: rgba(0,0,0,0.9); | |
padding: 15px; | |
border-radius: 8px; | |
z-index: 100; | |
font-size: 12px; | |
min-width: 180px; | |
} | |
#trafficStats { | |
position: absolute; | |
top: 50%; | |
right: 10px; | |
transform: translateY(-50%); | |
color: white; | |
background-color: rgba(0,0,0,0.9); | |
padding: 15px; | |
border-radius: 8px; | |
z-index: 100; | |
font-size: 12px; | |
min-width: 180px; | |
} | |
.highlight { color: #ffcc00; font-weight: bold; } | |
.success { color: #00ff00; font-weight: bold; } | |
.flocking { color: #00aaff; } | |
.solo { color: #ff8800; } | |
.leader { color: #ff00ff; font-weight: bold; } | |
.convoy { color: #00ffff; } | |
.parked { color: #88ff88; } | |
.species-0 { color: #ff6b6b; } | |
.species-1 { color: #4ecdc4; } | |
.species-2 { color: #45b7d1; } | |
.species-3 { color: #96ceb4; } | |
.species-4 { color: #ffd93d; } | |
.progress-bar { | |
width: 100%; | |
height: 10px; | |
background-color: #333; | |
border-radius: 5px; | |
overflow: hidden; | |
margin: 5px 0; | |
} | |
.progress-fill { | |
height: 100%; | |
background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1); | |
transition: width 0.3s ease; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="ui"> | |
<div class="highlight">AI Traffic Evolution Simulator</div> | |
<div>Epoch: <span id="epoch">1</span></div> | |
<div>Time: <span id="epochTime">60</span>s</div> | |
<div class="progress-bar"><div class="progress-fill" id="timeProgress"></div></div> | |
<div>Population: <span id="population">100</span></div> | |
<div>Species: <span id="speciesCount">1</span></div> | |
<div>Best Fitness: <span id="bestFitness">0</span></div> | |
<div>Traffic IQ: <span id="trafficIQ">50</span></div> | |
<div>Road Mastery: <span id="roadMastery">0</span>%</div> | |
</div> | |
<div id="controls"> | |
<button id="pauseBtn">Pause</button> | |
<button id="resetBtn">Reset</button> | |
<button id="speedBtn">Speed: 1x</button> | |
<button id="viewBtn">View: Overview</button> | |
<button id="flockBtn">Networks: ON</button> | |
<button id="trafficBtn">Traffic Rules: ON</button> | |
</div> | |
<div id="stats"> | |
<div><span class="highlight">Top Performers:</span></div> | |
<div id="topPerformers"></div> | |
<div style="margin-top: 10px;"><span class="highlight">Generation Stats:</span></div> | |
<div>Crashes: <span id="crashCount">0</span></div> | |
<div>Total Distance: <span id="totalDistance">0</span></div> | |
<div>Parking Events: <span id="parkingEvents">0</span></div> | |
<div>Lane Violations: <span id="laneViolations">0</span></div> | |
<div>Convoy Length: <span id="convoyLength">0</span></div> | |
</div> | |
<div id="flockingStats"> | |
<div><span class="highlight">Convoy Behavior:</span></div> | |
<div><span class="leader">Leaders:</span> <span id="leaderCount">0</span></div> | |
<div><span class="convoy">In Convoy:</span> <span id="convoyCount">0</span></div> | |
<div><span class="parked">Parked:</span> <span id="parkedCount">0</span></div> | |
<div><span class="solo">Solo:</span> <span id="soloCount">0</span></div> | |
<div>Largest Convoy: <span id="largestConvoy">0</span></div> | |
<div>Formation Quality: <span id="formationQuality">0</span>%</div> | |
<div>Parking Efficiency: <span id="parkingEfficiency">0</span>%</div> | |
</div> | |
<div id="trafficStats"> | |
<div><span class="highlight">Traffic Intelligence:</span></div> | |
<div>Lane Discipline: <span id="laneDiscipline">0</span>%</div> | |
<div>Following Distance: <span id="followingDistance">0</span>m</div> | |
<div>Road Adherence: <span id="roadAdherence">0</span>%</div> | |
<div>Turn Signals: <span id="turnSignals">0</span>%</div> | |
<div style="margin-top: 10px;"><span class="highlight">Parking:</span></div> | |
<div>Spots Occupied: <span id="spotsOccupied">0</span></div> | |
<div>Parking Success: <span id="parkingSuccess">0</span>%</div> | |
<div>Queue Efficiency: <span id="queueEfficiency">0</span>%</div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script> | |
// Global variables | |
let scene, camera, renderer, clock; | |
let world = { | |
roads: [], | |
intersections: [], | |
buildings: [], | |
parkingLots: [], | |
flockLines: [] | |
}; | |
// Enhanced evolution system | |
let epoch = 1; | |
let epochTime = 60; | |
let timeLeft = 60; | |
let population = []; | |
let species = []; | |
let populationSize = 100; | |
let bestFitness = 0; | |
let crashCount = 0; | |
let paused = false; | |
let speedMultiplier = 1; | |
let cameraMode = 'overview'; // 'overview', 'follow_best', 'follow_convoy' | |
let showFlockLines = true; | |
let trafficRules = true; | |
let parkingEvents = 0; | |
let laneViolations = 0; | |
// Traffic and road parameters | |
const ROAD_WIDTH = 12; | |
const LANE_WIDTH = 6; | |
const ROAD_SPACING = 150; | |
const FOLLOW_DISTANCE = 8; | |
const CONVOY_MAX_DISTANCE = 12; | |
const PARKING_SPOT_SIZE = { width: 4, length: 8 }; | |
// Enhanced Neural Network for traffic behavior | |
class TrafficAI { | |
constructor() { | |
this.inputSize = 28; // Enhanced traffic-aware inputs | |
this.hiddenLayers = [36, 28, 20]; | |
this.outputSize = 10; // More nuanced traffic outputs | |
this.memorySize = 8; | |
this.weights = []; | |
this.biases = []; | |
this.memory = new Array(this.memorySize).fill(0); | |
this.memoryPointer = 0; | |
// Build network | |
let prevSize = this.inputSize + this.memorySize; | |
for (let i = 0; i < this.hiddenLayers.length; i++) { | |
this.weights.push(this.randomMatrix(prevSize, this.hiddenLayers[i])); | |
this.biases.push(this.randomArray(this.hiddenLayers[i])); | |
prevSize = this.hiddenLayers[i]; | |
} | |
this.weights.push(this.randomMatrix(prevSize, this.outputSize)); | |
this.biases.push(this.randomArray(this.outputSize)); | |
// Traffic-specific traits | |
this.trafficTraits = { | |
laneKeeping: Math.random(), | |
followingBehavior: Math.random(), | |
parkingSkill: Math.random(), | |
convoyDiscipline: Math.random(), | |
roadPriority: Math.random() | |
}; | |
} | |
randomMatrix(rows, cols) { | |
let matrix = []; | |
for (let i = 0; i < rows; i++) { | |
matrix[i] = []; | |
for (let j = 0; j < cols; j++) { | |
matrix[i][j] = (Math.random() - 0.5) * 2; | |
} | |
} | |
return matrix; | |
} | |
randomArray(size) { | |
return Array(size).fill().map(() => (Math.random() - 0.5) * 2); | |
} | |
activate(inputs) { | |
let currentInput = [...inputs, ...this.memory]; | |
for (let layer = 0; layer < this.hiddenLayers.length; layer++) { | |
currentInput = this.forwardLayer(currentInput, this.weights[layer], this.biases[layer]); | |
} | |
const outputs = this.forwardLayer(currentInput, | |
this.weights[this.weights.length - 1], | |
this.biases[this.biases.length - 1]); | |
this.updateMemory(inputs, outputs); | |
return outputs; | |
} | |
forwardLayer(inputs, weights, biases) { | |
const outputs = new Array(weights[0].length).fill(0); | |
for (let i = 0; i < outputs.length; i++) { | |
for (let j = 0; j < inputs.length; j++) { | |
outputs[i] += inputs[j] * weights[j][i]; | |
} | |
outputs[i] += biases[i]; | |
outputs[i] = this.sigmoid(outputs[i]); | |
} | |
return outputs; | |
} | |
sigmoid(x) { | |
return 1 / (1 + Math.exp(-Math.max(-10, Math.min(10, x)))); | |
} | |
updateMemory(inputs, outputs) { | |
const roadInfo = inputs.slice(20, 24).reduce((a, b) => a + b, 0) / 4; | |
this.memory[this.memoryPointer] = roadInfo; | |
this.memoryPointer = (this.memoryPointer + 1) % this.memorySize; | |
} | |
mutate(rate = 0.1) { | |
this.weights.forEach(weightMatrix => { | |
this.mutateMatrix(weightMatrix, rate); | |
}); | |
this.biases.forEach(biasArray => { | |
this.mutateArray(biasArray, rate); | |
}); | |
Object.keys(this.trafficTraits).forEach(trait => { | |
if (Math.random() < rate) { | |
this.trafficTraits[trait] += (Math.random() - 0.5) * 0.2; | |
this.trafficTraits[trait] = Math.max(0, Math.min(1, this.trafficTraits[trait])); | |
} | |
}); | |
} | |
mutateMatrix(matrix, rate) { | |
for (let i = 0; i < matrix.length; i++) { | |
for (let j = 0; j < matrix[i].length; j++) { | |
if (Math.random() < rate) { | |
matrix[i][j] += (Math.random() - 0.5) * 0.5; | |
matrix[i][j] = Math.max(-3, Math.min(3, matrix[i][j])); | |
} | |
} | |
} | |
} | |
mutateArray(array, rate) { | |
for (let i = 0; i < array.length; i++) { | |
if (Math.random() < rate) { | |
array[i] += (Math.random() - 0.5) * 0.5; | |
array[i] = Math.max(-3, Math.min(3, array[i])); | |
} | |
} | |
} | |
copy() { | |
const newAI = new TrafficAI(); | |
newAI.weights = this.weights.map(matrix => matrix.map(row => [...row])); | |
newAI.biases = this.biases.map(bias => [...bias]); | |
newAI.memory = [...this.memory]; | |
newAI.memoryPointer = this.memoryPointer; | |
newAI.trafficTraits = {...this.trafficTraits}; | |
return newAI; | |
} | |
} | |
// Enhanced AI Car with traffic behavior | |
class TrafficCar { | |
constructor(x = 0, z = 0) { | |
this.brain = new TrafficAI(); | |
this.mesh = this.createCarMesh(); | |
this.mesh.position.set(x, 1, z); | |
// Movement and traffic properties | |
this.velocity = new THREE.Vector3(); | |
this.acceleration = new THREE.Vector3(); | |
this.maxSpeed = 20; | |
this.minSpeed = 2; | |
this.currentLane = null; | |
this.targetLane = null; | |
this.lanePosition = 0; // -1 to 1 within lane | |
// Road transition tracking | |
this.lastRoadPosition = 0; | |
this.isTransitioningToRoad = false; | |
this.roadTransitionTime = 0; | |
// Convoy and flock behavior | |
this.flockId = -1; | |
this.convoyPosition = -1; // Position in convoy (-1 = not in convoy) | |
this.convoyLeader = null; | |
this.convoyFollowers = []; | |
this.followTarget = null; | |
this.role = 'driver'; // driver, leader, parker | |
// Enhanced parking system | |
this.isParked = false; | |
this.parkingSpot = null; | |
this.targetParkingLot = null; | |
this.parkingQueue = -1; // Position in parking queue (-1 = not queued) | |
this.isParkingApproach = false; | |
this.isInApproachLane = false; | |
this.isInExitLane = false; | |
this.approachTarget = null; | |
this.exitTarget = null; | |
this.parkingAttempts = 0; | |
this.maxParkingAttempts = 3; | |
this.departureTime = 0; | |
this.turnSignal = 'none'; // left, right, none | |
this.laneDiscipline = 0; | |
this.followingDistance = FOLLOW_DISTANCE; | |
// Fitness and metrics | |
this.fitness = 0; | |
this.roadTime = 0; | |
this.convoyTime = 0; | |
this.parkingScore = 0; | |
this.trafficViolations = 0; | |
this.distanceTraveled = 0; | |
this.crashed = false; | |
this.timeAlive = 100; | |
// Sensors and visualization | |
this.sensors = Array(16).fill(0); | |
this.roadSensors = Array(8).fill(0); | |
this.trafficSensors = Array(4).fill(0); | |
this.sensorRays = []; | |
this.flockLines = []; | |
this.neighbors = []; | |
this.lastPosition = new THREE.Vector3(x, 1, z); | |
this.createSensorRays(); | |
this.createFlockVisualization(); | |
this.initializeMovement(); | |
} | |
createCarMesh() { | |
const group = new THREE.Group(); | |
// Car body | |
const bodyGeometry = new THREE.BoxGeometry(1.5, 0.8, 3.5); | |
this.bodyMaterial = new THREE.MeshLambertMaterial({ | |
color: new THREE.Color().setHSL(Math.random(), 0.8, 0.6) | |
}); | |
const body = new THREE.Mesh(bodyGeometry, this.bodyMaterial); | |
body.position.y = 0.4; | |
body.castShadow = true; | |
group.add(body); | |
// Turn signals | |
const signalGeometry = new THREE.SphereGeometry(0.15, 6, 4); | |
this.leftSignal = new THREE.Mesh(signalGeometry, | |
new THREE.MeshLambertMaterial({ color: 0xff8800, transparent: true, opacity: 0.5 })); | |
this.leftSignal.position.set(-0.8, 0.8, 1.2); | |
group.add(this.leftSignal); | |
this.rightSignal = new THREE.Mesh(signalGeometry, | |
new THREE.MeshLambertMaterial({ color: 0xff8800, transparent: true, opacity: 0.5 })); | |
this.rightSignal.position.set(0.8, 0.8, 1.2); | |
group.add(this.rightSignal); | |
// Role indicator | |
const indicatorGeometry = new THREE.ConeGeometry(0.2, 0.8, 6); | |
this.roleIndicator = new THREE.Mesh(indicatorGeometry, | |
new THREE.MeshLambertMaterial({ color: 0xffffff })); | |
this.roleIndicator.position.set(0, 1.5, 0); | |
group.add(this.roleIndicator); | |
// Wheels with proper rotation | |
const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 8); | |
const wheelMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); | |
this.wheels = []; | |
const wheelPositions = [ | |
[-0.7, 0, 1.4], [0.7, 0, 1.4], | |
[-0.7, 0, -1.4], [0.7, 0, -1.4] | |
]; | |
wheelPositions.forEach((pos, i) => { | |
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
wheel.position.set(...pos); | |
wheel.rotation.z = Math.PI / 2; | |
this.wheels.push(wheel); | |
group.add(wheel); | |
}); | |
return group; | |
} | |
createSensorRays() { | |
const sensorMaterial = new THREE.LineBasicMaterial({ | |
color: 0xff0000, | |
transparent: true, | |
opacity: 0.2 | |
}); | |
for (let i = 0; i < 16; i++) { | |
const geometry = new THREE.BufferGeometry().setFromPoints([ | |
new THREE.Vector3(0, 0, 0), | |
new THREE.Vector3(0, 0, 10) | |
]); | |
const ray = new THREE.Line(geometry, sensorMaterial); | |
this.sensorRays.push(ray); | |
this.mesh.add(ray); | |
} | |
} | |
createFlockVisualization() { | |
const flockMaterial = new THREE.LineBasicMaterial({ | |
color: 0x00ff00, | |
transparent: true, | |
opacity: 0.6, | |
linewidth: 2 | |
}); | |
for (let i = 0; i < 10; i++) { | |
const geometry = new THREE.BufferGeometry().setFromPoints([ | |
new THREE.Vector3(0, 2, 0), | |
new THREE.Vector3(0, 2, 0) | |
]); | |
const line = new THREE.Line(geometry, flockMaterial); | |
this.flockLines.push(line); | |
if (showFlockLines) scene.add(line); | |
} | |
} | |
initializeMovement() { | |
// Start on a road if possible | |
const nearestRoad = this.findNearestRoad(); | |
if (nearestRoad) { | |
this.currentLane = nearestRoad.lane; | |
this.mesh.rotation.y = nearestRoad.direction; | |
this.velocity.set( | |
Math.sin(nearestRoad.direction) * 8, | |
0, | |
Math.cos(nearestRoad.direction) * 8 | |
); | |
} else { | |
this.mesh.rotation.y = Math.random() * Math.PI * 2; | |
this.velocity.set( | |
Math.sin(this.mesh.rotation.y) * 6, | |
0, | |
Math.cos(this.mesh.rotation.y) * 6 | |
); | |
} | |
} | |
findNearestRoad() { | |
const pos = this.mesh.position; | |
let nearestRoad = null; | |
let minDistance = Infinity; | |
// Check major highways (8 lanes - 48 units wide) | |
const mainHighwayPositions = [0]; // Main cross highways | |
mainHighwayPositions.forEach(roadPos => { | |
// Horizontal highway | |
const distToHorizontal = Math.abs(pos.z - roadPos); | |
if (distToHorizontal < 24 && distToHorizontal < minDistance) { | |
minDistance = distToHorizontal; | |
const laneCenter = roadPos + (pos.x > 0 ? -12 : 12); // 4 lanes each direction | |
nearestRoad = { | |
lane: 'highway_horizontal', | |
center: laneCenter, | |
direction: pos.x > 0 ? Math.PI : 0, | |
width: 48 | |
}; | |
} | |
// Vertical highway | |
const distToVertical = Math.abs(pos.x - roadPos); | |
if (distToVertical < 24 && distToVertical < minDistance) { | |
minDistance = distToVertical; | |
const laneCenter = roadPos + (pos.z > 0 ? -12 : 12); | |
nearestRoad = { | |
lane: 'highway_vertical', | |
center: laneCenter, | |
direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2, | |
width: 48 | |
}; | |
} | |
}); | |
// Check secondary highways (4 lanes - 24 units wide) | |
for (let roadPos = -300; roadPos <= 300; roadPos += 150) { | |
if (roadPos === 0) continue; // Skip main highway | |
const distToHorizontal = Math.abs(pos.z - roadPos); | |
if (distToHorizontal < 12 && distToHorizontal < minDistance) { | |
minDistance = distToHorizontal; | |
const laneCenter = roadPos + (pos.x > 0 ? -6 : 6); | |
nearestRoad = { | |
lane: 'secondary_horizontal', | |
center: laneCenter, | |
direction: pos.x > 0 ? Math.PI : 0, | |
width: 24 | |
}; | |
} | |
const distToVertical = Math.abs(pos.x - roadPos); | |
if (distToVertical < 12 && distToVertical < minDistance) { | |
minDistance = distToVertical; | |
const laneCenter = roadPos + (pos.z > 0 ? -6 : 6); | |
nearestRoad = { | |
lane: 'secondary_vertical', | |
center: laneCenter, | |
direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2, | |
width: 24 | |
}; | |
} | |
} | |
// Check local roads (2 lanes - 12 units wide) | |
for (let roadPos = -375; roadPos <= 375; roadPos += 75) { | |
const distToHorizontal = Math.abs(pos.z - roadPos); | |
if (distToHorizontal < 6 && distToHorizontal < minDistance) { | |
minDistance = distToHorizontal; | |
const laneCenter = roadPos + (pos.x > 0 ? -3 : 3); | |
nearestRoad = { | |
lane: 'local_horizontal', | |
center: laneCenter, | |
direction: pos.x > 0 ? Math.PI : 0, | |
width: 12 | |
}; | |
} | |
const distToVertical = Math.abs(pos.x - roadPos); | |
if (distToVertical < 6 && distToVertical < minDistance) { | |
minDistance = distToVertical; | |
const laneCenter = roadPos + (pos.z > 0 ? -3 : 3); | |
nearestRoad = { | |
lane: 'local_vertical', | |
center: laneCenter, | |
direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2, | |
width: 12 | |
}; | |
} | |
} | |
// Check building access roads (less precise matching) | |
if (!nearestRoad || minDistance > 8) { | |
world.buildings.forEach(building => { | |
const buildingPos = building.mesh.position; | |
const buildingBox = new THREE.Box3().setFromObject(building.mesh); | |
const size = buildingBox.getSize(new THREE.Vector3()); | |
// Check proximity to building access roads | |
const accessRoadPositions = [ | |
{ x: buildingPos.x, z: buildingPos.z + size.z/2 + 10, dir: 'horizontal' }, | |
{ x: buildingPos.x, z: buildingPos.z - size.z/2 - 10, dir: 'horizontal' }, | |
{ x: buildingPos.x + size.x/2 + 10, z: buildingPos.z, dir: 'vertical' }, | |
{ x: buildingPos.x - size.x/2 - 10, z: buildingPos.z, dir: 'vertical' } | |
]; | |
accessRoadPositions.forEach(accessRoad => { | |
const dist = pos.distanceTo(new THREE.Vector3(accessRoad.x, 0, accessRoad.z)); | |
if (dist < 8 && dist < minDistance) { | |
minDistance = dist; | |
nearestRoad = { | |
lane: `access_${accessRoad.dir}`, | |
center: accessRoad.dir === 'horizontal' ? accessRoad.z : accessRoad.x, | |
direction: accessRoad.dir === 'horizontal' ? | |
(pos.x > accessRoad.x ? Math.PI : 0) : | |
(pos.z > accessRoad.z ? -Math.PI/2 : Math.PI/2), | |
width: 8 | |
}; | |
} | |
}); | |
}); | |
} | |
return nearestRoad; | |
} | |
getRoadPosition() { | |
const pos = this.mesh.position; | |
let maxRoadScore = 0; | |
// Check all road types for best road position score | |
// Major highways | |
const mainHighwayDist = Math.abs(pos.z); | |
if (mainHighwayDist <= 24) { | |
maxRoadScore = Math.max(maxRoadScore, 1 - (mainHighwayDist / 24)); | |
} | |
const mainVerticalDist = Math.abs(pos.x); | |
if (mainVerticalDist <= 24) { | |
maxRoadScore = Math.max(maxRoadScore, 1 - (mainVerticalDist / 24)); | |
} | |
// Secondary highways | |
for (let roadPos = -300; roadPos <= 300; roadPos += 150) { | |
if (roadPos === 0) continue; | |
const hDist = Math.abs(pos.z - roadPos); | |
if (hDist <= 12) { | |
maxRoadScore = Math.max(maxRoadScore, 1 - (hDist / 12)); | |
} | |
const vDist = Math.abs(pos.x - roadPos); | |
if (vDist <= 12) { | |
maxRoadScore = Math.max(maxRoadScore, 1 - (vDist / 12)); | |
} | |
} | |
// Local roads | |
for (let roadPos = -375; roadPos <= 375; roadPos += 75) { | |
const hDist = Math.abs(pos.z - roadPos); | |
if (hDist <= 6) { | |
maxRoadScore = Math.max(maxRoadScore, 1 - (hDist / 6)); | |
} | |
const vDist = Math.abs(pos.x - roadPos); | |
if (vDist <= 6) { | |
maxRoadScore = Math.max(maxRoadScore, 1 - (vDist / 6)); | |
} | |
} | |
return maxRoadScore; | |
} | |
updateSensors() { | |
const maxDistance = 10; | |
const raycaster = new THREE.Raycaster(); | |
// 16-direction obstacle sensors | |
for (let i = 0; i < 16; i++) { | |
const angle = (i * Math.PI * 2) / 16; | |
const direction = new THREE.Vector3( | |
Math.sin(angle), 0, Math.cos(angle) | |
); | |
direction.applyQuaternion(this.mesh.quaternion); | |
raycaster.set(this.mesh.position, direction); | |
const intersects = raycaster.intersectObjects(this.getObstacles(), true); | |
if (intersects.length > 0 && intersects[0].distance <= maxDistance) { | |
this.sensors[i] = 1 - (intersects[0].distance / maxDistance); | |
} else { | |
this.sensors[i] = 0; | |
} | |
// Update visual rays | |
const endDistance = intersects.length > 0 ? | |
Math.min(intersects[0].distance, maxDistance) : maxDistance; | |
const rayEnd = direction.clone().multiplyScalar(endDistance); | |
this.sensorRays[i].geometry.setFromPoints([ | |
new THREE.Vector3(0, 0, 0), rayEnd | |
]); | |
} | |
// Road-specific sensors | |
this.updateRoadSensors(); | |
this.updateTrafficSensors(); | |
} | |
updateRoadSensors() { | |
const pos = this.mesh.position; | |
// Road position and lane detection | |
this.roadSensors[0] = this.getRoadPosition(); | |
this.roadSensors[1] = this.getLanePosition(); | |
this.roadSensors[2] = this.getRoadDirection(); | |
this.roadSensors[3] = this.getDistanceToIntersection(); | |
// Parking lot detection | |
this.roadSensors[4] = this.getNearestParkingLot(); | |
this.roadSensors[5] = this.getParkingAvailability(); | |
// Traffic flow | |
this.roadSensors[6] = this.getTrafficDensity(); | |
this.roadSensors[7] = this.getOptimalSpeed(); | |
} | |
updateTrafficSensors() { | |
// Convoy and following behavior | |
this.trafficSensors[0] = this.getConvoyStatus(); | |
this.trafficSensors[1] = this.getFollowingDistance(); | |
this.trafficSensors[2] = this.getLeaderDistance(); | |
this.trafficSensors[3] = this.getNeedToPark(); | |
} | |
getRoadPosition() { | |
const pos = this.mesh.position; | |
// Check horizontal roads | |
for (let roadZ = -300; roadZ <= 300; roadZ += ROAD_SPACING) { | |
const distToRoad = Math.abs(pos.z - roadZ); | |
if (distToRoad <= ROAD_WIDTH / 2) { | |
return 1 - (distToRoad / (ROAD_WIDTH / 2)); | |
} | |
} | |
// Check vertical roads | |
for (let roadX = -300; roadX <= 300; roadX += ROAD_SPACING) { | |
const distToRoad = Math.abs(pos.x - roadX); | |
if (distToRoad <= ROAD_WIDTH / 2) { | |
return 1 - (distToRoad / (ROAD_WIDTH / 2)); | |
} | |
} | |
return 0; // Off road | |
} | |
getLanePosition() { | |
const pos = this.mesh.position; | |
const roadInfo = this.findNearestRoad(); | |
if (!roadInfo) return 0.5; | |
if (roadInfo.lane === 'horizontal') { | |
const laneOffset = pos.z - roadInfo.center; | |
return 0.5 + (laneOffset / LANE_WIDTH); | |
} else { | |
const laneOffset = pos.x - roadInfo.center; | |
return 0.5 + (laneOffset / LANE_WIDTH); | |
} | |
} | |
getRoadDirection() { | |
const roadInfo = this.findNearestRoad(); | |
if (!roadInfo) return 0.5; | |
const currentDirection = Math.atan2(this.velocity.x, this.velocity.z); | |
const targetDirection = roadInfo.direction; | |
let angleDiff = targetDirection - currentDirection; | |
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; | |
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; | |
return 0.5 + (angleDiff / Math.PI) * 0.5; | |
} | |
getDistanceToIntersection() { | |
const pos = this.mesh.position; | |
let minDist = Infinity; | |
// Find distance to nearest intersection | |
for (let x = -300; x <= 300; x += ROAD_SPACING) { | |
for (let z = -300; z <= 300; z += ROAD_SPACING) { | |
const dist = pos.distanceTo(new THREE.Vector3(x, 0, z)); | |
minDist = Math.min(minDist, dist); | |
} | |
} | |
return Math.max(0, 1 - minDist / 50); | |
} | |
getNearestParkingLot() { | |
const pos = this.mesh.position; | |
let nearestDist = Infinity; | |
world.parkingLots.forEach(lot => { | |
const dist = pos.distanceTo(lot.center); | |
if (dist < nearestDist) { | |
nearestDist = dist; | |
this.targetParkingLot = lot; | |
} | |
}); | |
return Math.max(0, 1 - nearestDist / 100); | |
} | |
getParkingAvailability() { | |
if (!this.targetParkingLot) return 0; | |
const availableSpots = this.targetParkingLot.spots.filter(spot => !spot.occupied).length; | |
return availableSpots / this.targetParkingLot.spots.length; | |
} | |
getTrafficDensity() { | |
const pos = this.mesh.position; | |
let nearbyCount = 0; | |
population.forEach(other => { | |
if (other !== this && !other.crashed && !other.isParked) { | |
const dist = pos.distanceTo(other.mesh.position); | |
if (dist < 30) nearbyCount++; | |
} | |
}); | |
return Math.min(nearbyCount / 10, 1); | |
} | |
getOptimalSpeed() { | |
const roadPos = this.getRoadPosition(); | |
const density = this.getTrafficDensity(); | |
return roadPos * (1 - density * 0.5); | |
} | |
getConvoyStatus() { | |
return this.convoyPosition >= 0 ? 1 : 0; | |
} | |
getFollowingDistance() { | |
if (!this.followTarget) return 1; | |
const dist = this.mesh.position.distanceTo(this.followTarget.mesh.position); | |
return Math.min(dist / 20, 1); | |
} | |
getLeaderDistance() { | |
if (!this.convoyLeader) return 0; | |
const dist = this.mesh.position.distanceTo(this.convoyLeader.mesh.position); | |
return Math.max(0, 1 - dist / 50); | |
} | |
getNeedToPark() { | |
return (this.timeAlive < 30 && !this.isParked) ? 1 : 0; | |
} | |
updateConvoyBehavior() { | |
this.neighbors = []; | |
// Find nearby cars for convoy formation | |
population.forEach(other => { | |
if (other !== this && !other.crashed && !other.isParked) { | |
const distance = this.mesh.position.distanceTo(other.mesh.position); | |
if (distance < 25) { | |
this.neighbors.push(other); | |
} | |
} | |
}); | |
// Calculate flock centroid and check if moving away | |
this.updateFlockCohesion(); | |
// Determine role and convoy behavior | |
this.updateRole(); | |
this.updateConvoyFormation(); | |
} | |
updateFlockCohesion() { | |
if (this.neighbors.length === 0) return; | |
// Calculate flock centroid | |
const centroid = new THREE.Vector3(); | |
this.neighbors.forEach(neighbor => { | |
centroid.add(neighbor.mesh.position); | |
}); | |
centroid.divideScalar(this.neighbors.length); | |
// Check if moving away from centroid | |
const currentPos = this.mesh.position.clone(); | |
const futurePos = currentPos.clone().add(this.velocity.clone().normalize().multiplyScalar(5)); | |
const currentDistToCentroid = currentPos.distanceTo(centroid); | |
const futureDistToCentroid = futurePos.distanceTo(centroid); | |
// If moving away from flock, apply gentle correction | |
if (futureDistToCentroid > currentDistToCentroid && currentDistToCentroid > 15) { | |
const returnForce = centroid.clone().sub(currentPos).normalize().multiplyScalar(0.3); | |
this.velocity.add(returnForce); | |
// Gentle turning toward centroid | |
const targetDirection = Math.atan2(returnForce.x, returnForce.z); | |
const currentDirection = Math.atan2(this.velocity.x, this.velocity.z); | |
let angleDiff = targetDirection - currentDirection; | |
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; | |
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; | |
// Apply gentle turning | |
this.mesh.rotation.y += angleDiff * 0.02; | |
// Slow down for turning | |
this.velocity.multiplyScalar(0.98); | |
} | |
// Move flock centroid toward roads | |
this.guideFlockToRoads(centroid); | |
} | |
guideFlockToRoads(centroid) { | |
if (this.role !== 'leader') return; | |
// Find nearest road from centroid | |
const nearestRoadInfo = this.findNearestRoadFromPosition(centroid); | |
if (!nearestRoadInfo) return; | |
const roadDistance = nearestRoadInfo.distance; | |
if (roadDistance > 5) { | |
// Guide flock toward road | |
const roadDirection = nearestRoadInfo.direction; | |
const roadForce = roadDirection.clone().multiplyScalar(0.2); | |
this.velocity.add(roadForce); | |
} | |
} | |
findNearestRoadFromPosition(position) { | |
let nearestRoad = null; | |
let minDistance = Infinity; | |
// Check horizontal roads | |
for (let roadZ = -300; roadZ <= 300; roadZ += ROAD_SPACING) { | |
const distToRoad = Math.abs(position.z - roadZ); | |
if (distToRoad < minDistance) { | |
minDistance = distToRoad; | |
const direction = new THREE.Vector3(0, 0, roadZ - position.z).normalize(); | |
nearestRoad = { | |
distance: distToRoad, | |
direction: direction, | |
lane: 'horizontal', | |
center: roadZ | |
}; | |
} | |
} | |
// Check vertical roads | |
for (let roadX = -300; roadX <= 300; roadX += ROAD_SPACING) { | |
const distToRoad = Math.abs(position.x - roadX); | |
if (distToRoad < minDistance) { | |
minDistance = distToRoad; | |
const direction = new THREE.Vector3(roadX - position.x, 0, 0).normalize(); | |
nearestRoad = { | |
distance: distToRoad, | |
direction: direction, | |
lane: 'vertical', | |
center: roadX | |
}; | |
} | |
} | |
return nearestRoad; | |
} | |
updateRole() { | |
const roadPos = this.getRoadPosition(); | |
if (roadPos > 0.8 && this.neighbors.length > 2 && this.brain.trafficTraits.convoyDiscipline > 0.7) { | |
this.role = 'leader'; | |
this.roleIndicator.material.color.setHex(0xff00ff); | |
} else if (this.getNeedToPark() > 0.5) { | |
this.role = 'parker'; | |
this.roleIndicator.material.color.setHex(0x00ff00); | |
} else { | |
this.role = 'driver'; | |
this.roleIndicator.material.color.setHex(0xffffff); | |
} | |
} | |
updateConvoyFormation() { | |
if (this.role === 'leader') { | |
// Leaders organize convoys | |
this.convoyFollowers = this.neighbors | |
.filter(car => car.role === 'driver') | |
.sort((a, b) => | |
this.mesh.position.distanceTo(a.mesh.position) - | |
this.mesh.position.distanceTo(b.mesh.position) | |
) | |
.slice(0, 5); // Max 5 followers | |
this.convoyFollowers.forEach((follower, index) => { | |
follower.convoyLeader = this; | |
follower.convoyPosition = index; | |
follower.followTarget = index === 0 ? this : this.convoyFollowers[index - 1]; | |
}); | |
} else if (this.convoyPosition >= 0) { | |
// Update following behavior | |
if (this.followTarget && this.followTarget.crashed) { | |
this.convoyPosition = -1; | |
this.convoyLeader = null; | |
this.followTarget = null; | |
} | |
} | |
} | |
getEnhancedInputs() { | |
return [ | |
...this.sensors, // 16 obstacle sensors | |
...this.roadSensors, // 8 road/navigation sensors | |
...this.trafficSensors, // 4 traffic behavior sensors | |
]; | |
} | |
update(deltaTime) { | |
// Handle parked cars separately | |
if (this.isParked) { | |
this.handleParkedBehavior(deltaTime); | |
return; | |
} | |
if (this.crashed) return; | |
this.timeAlive -= deltaTime; | |
if (this.timeAlive <= 0 || this.parkingAttempts >= this.maxParkingAttempts) { | |
if (!this.isParkingApproach) { | |
this.attemptParking(); | |
} | |
return; | |
} | |
this.updateSensors(); | |
this.updateConvoyBehavior(); | |
this.updateVisuals(); | |
// Get AI decision | |
const inputs = this.getEnhancedInputs(); | |
const outputs = this.brain.activate(inputs); | |
// Apply traffic-aware movement | |
this.applyTrafficMovement(outputs, deltaTime); | |
this.updateFitness(deltaTime); | |
this.lastPosition.copy(this.mesh.position); | |
this.checkCollisions(); | |
this.keepInBounds(); | |
} | |
applyTrafficMovement(outputs, deltaTime) { | |
const [ | |
acceleration, braking, steerLeft, steerRight, | |
laneChange, followConvoy, parkingManeuver, turnSignalLeft, | |
turnSignalRight, emergencyStop | |
] = outputs; | |
// Update turn signals | |
this.turnSignal = 'none'; | |
if (turnSignalLeft > 0.7) this.turnSignal = 'left'; | |
if (turnSignalRight > 0.7) this.turnSignal = 'right'; | |
this.leftSignal.material.opacity = this.turnSignal === 'left' ? 1.0 : 0.3; | |
this.rightSignal.material.opacity = this.turnSignal === 'right' ? 1.0 : 0.3; | |
// Emergency stop | |
if (emergencyStop > 0.8) { | |
this.velocity.multiplyScalar(0.7); | |
return; | |
} | |
// Parking maneuver | |
if (parkingManeuver > 0.8 && this.targetParkingLot) { | |
this.executeParking(deltaTime); | |
return; | |
} | |
// Road-following behavior | |
this.followRoad(deltaTime); | |
// Convoy following | |
if (followConvoy > 0.6 && this.followTarget) { | |
this.followConvoyTarget(deltaTime); | |
} | |
// Basic movement | |
const forward = new THREE.Vector3(0, 0, 1); | |
forward.applyQuaternion(this.mesh.quaternion); | |
// Acceleration and braking | |
if (acceleration > 0.3) { | |
this.velocity.add(forward.multiplyScalar(acceleration * 8 * deltaTime)); | |
} | |
if (braking > 0.5) { | |
this.velocity.multiplyScalar(1 - braking * deltaTime * 3); | |
} | |
// Steering | |
const steering = (steerRight - steerLeft) * 0.08 * deltaTime; | |
this.mesh.rotation.y += steering; | |
// Speed limits | |
const currentSpeed = this.velocity.length(); | |
if (currentSpeed > this.maxSpeed) { | |
this.velocity.normalize().multiplyScalar(this.maxSpeed); | |
} else if (currentSpeed < this.minSpeed && currentSpeed > 0.1) { | |
this.velocity.normalize().multiplyScalar(this.minSpeed); | |
} | |
// Apply movement | |
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime)); | |
// Wheel rotation | |
this.wheels.forEach(wheel => { | |
wheel.rotation.x += currentSpeed * deltaTime * 0.1; | |
}); | |
} | |
followRoad(deltaTime) { | |
const roadInfo = this.findNearestRoad(); | |
const currentRoadPos = this.getRoadPosition(); | |
// Check if transitioning from grass to road | |
if (currentRoadPos > 0.3 && this.lastRoadPosition <= 0.3) { | |
// Just touched road after being on grass - slow down | |
this.velocity.multiplyScalar(0.7); | |
this.isTransitioningToRoad = true; | |
this.roadTransitionTime = 2.0; // 2 seconds to stabilize | |
} | |
if (this.roadTransitionTime > 0) { | |
this.roadTransitionTime -= deltaTime; | |
// Maintain slower speed during transition | |
const currentSpeed = this.velocity.length(); | |
if (currentSpeed > this.maxSpeed * 0.6) { | |
this.velocity.normalize().multiplyScalar(this.maxSpeed * 0.6); | |
} | |
} | |
this.lastRoadPosition = currentRoadPos; | |
if (!roadInfo) return; | |
// Enhanced lane-keeping force | |
const laneKeepingForce = this.brain.trafficTraits.laneKeeping; | |
let targetDirection = roadInfo.direction; | |
// Adjust for lane position with stronger correction | |
if (roadInfo.lane === 'horizontal') { | |
const laneOffset = this.mesh.position.z - roadInfo.center; | |
if (Math.abs(laneOffset) > LANE_WIDTH / 4) { | |
const correction = -laneOffset * 0.05 * laneKeepingForce; | |
this.velocity.z += correction; | |
if (Math.abs(laneOffset) > LANE_WIDTH / 2) { | |
this.trafficViolations++; | |
laneViolations++; | |
} | |
} | |
} else { | |
const laneOffset = this.mesh.position.x - roadInfo.center; | |
if (Math.abs(laneOffset) > LANE_WIDTH / 4) { | |
const correction = -laneOffset * 0.05 * laneKeepingForce; | |
this.velocity.x += correction; | |
if (Math.abs(laneOffset) > LANE_WIDTH / 2) { | |
this.trafficViolations++; | |
laneViolations++; | |
} | |
} | |
} | |
// Stronger direction alignment for road following | |
const currentDirection = Math.atan2(this.velocity.x, this.velocity.z); | |
let angleDiff = targetDirection - currentDirection; | |
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; | |
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; | |
if (Math.abs(angleDiff) > 0.1) { | |
this.mesh.rotation.y += angleDiff * 0.08 * laneKeepingForce; | |
} | |
this.roadTime += deltaTime; | |
this.laneDiscipline = Math.max(0, 1 - this.trafficViolations * 0.1); | |
} | |
followConvoyTarget(deltaTime) { | |
if (!this.followTarget) return; | |
const targetPos = this.followTarget.mesh.position; | |
const distance = this.mesh.position.distanceTo(targetPos); | |
const idealDistance = FOLLOW_DISTANCE + (this.convoyPosition * 2); | |
if (distance > idealDistance + 3) { | |
// Too far - speed up | |
const catchUpForce = this.followTarget.velocity.clone().multiplyScalar(0.3); | |
this.velocity.add(catchUpForce.multiplyScalar(deltaTime)); | |
} else if (distance < idealDistance - 2) { | |
// Too close - slow down | |
this.velocity.multiplyScalar(0.95); | |
} | |
// Align with target's direction | |
const targetDirection = Math.atan2(this.followTarget.velocity.x, this.followTarget.velocity.z); | |
const currentDirection = Math.atan2(this.velocity.x, this.velocity.z); | |
let angleDiff = targetDirection - currentDirection; | |
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; | |
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; | |
this.mesh.rotation.y += angleDiff * 0.1; | |
this.convoyTime += deltaTime; | |
this.followingDistance = distance; | |
} | |
executeParking(deltaTime) { | |
if (!this.targetParkingLot) { | |
this.findNearestParkingLot(); | |
return; | |
} | |
// Enhanced parking state machine | |
if (this.parkingQueue === -1) { | |
this.joinParkingQueue(); | |
} | |
if (this.isInApproachLane) { | |
this.handleApproachLane(deltaTime); | |
} else if (this.canProceedToApproach()) { | |
this.enterApproachLane(deltaTime); | |
} else { | |
this.waitForApproachAccess(deltaTime); | |
} | |
} | |
joinParkingQueue() { | |
if (!this.targetParkingLot) return; | |
if (!this.targetParkingLot.queue) { | |
this.targetParkingLot.queue = []; | |
} | |
if (!this.targetParkingLot.queue.includes(this)) { | |
this.targetParkingLot.queue.push(this); | |
this.parkingQueue = this.targetParkingLot.queue.length - 1; | |
} | |
} | |
canProceedToApproach() { | |
if (!this.targetParkingLot || !this.targetParkingLot.queue) return false; | |
const queuePosition = this.targetParkingLot.queue.indexOf(this); | |
// Check if there's space in approach lane | |
const approachLaneOccupancy = population.filter(car => | |
car.isInApproachLane && car.targetParkingLot === this.targetParkingLot | |
).length; | |
return queuePosition < 3 && approachLaneOccupancy < this.targetParkingLot.approachLane.length; | |
} | |
enterApproachLane(deltaTime) { | |
// Find first available approach lane position | |
const approachPositions = this.targetParkingLot.approachLane; | |
let targetPosition = null; | |
for (let i = 0; i < approachPositions.length; i++) { | |
const pos = approachPositions[i]; | |
const occupied = population.some(car => | |
car !== this && | |
car.isInApproachLane && | |
car.mesh.position.distanceTo(pos) < 3 | |
); | |
if (!occupied) { | |
targetPosition = pos; | |
break; | |
} | |
} | |
if (targetPosition) { | |
this.isInApproachLane = true; | |
this.approachTarget = targetPosition; | |
this.moveToPosition(targetPosition, deltaTime, 4); // Slow approach | |
} | |
} | |
handleApproachLane(deltaTime) { | |
// Check if we can proceed to actual parking | |
const availableSpot = this.targetParkingLot.spots.find(spot => !spot.occupied); | |
if (!availableSpot) { | |
// Wait in approach lane | |
this.velocity.multiplyScalar(0.95); | |
return; | |
} | |
const spotDistance = this.mesh.position.distanceTo(availableSpot.position); | |
if (spotDistance < 3) { | |
// Successfully park | |
this.completeParkingProcess(availableSpot); | |
} else { | |
// Move toward spot | |
this.moveToPosition(availableSpot.position, deltaTime, 2); | |
} | |
} | |
completeParkingProcess(spot) { | |
this.isParked = true; | |
this.parkingSpot = spot; | |
spot.occupied = true; | |
spot.car = this; | |
this.mesh.position.copy(spot.position); | |
this.velocity.set(0, 0, 0); | |
this.parkingScore += 100; | |
this.leaveParkingQueue(); | |
this.isInApproachLane = false; | |
parkingEvents++; | |
this.departureTime = 15 + Math.random() * 25; | |
this.updateCarColor(); | |
} | |
waitForApproachAccess(deltaTime) { | |
// Line up end-to-end near parking lot | |
const queuePosition = Math.min(this.parkingQueue, 5); | |
const queueTarget = this.targetParkingLot.center.clone(); | |
queueTarget.add(new THREE.Vector3(-50, 0, -15 + queuePosition * 5)); // 5m spacing | |
this.moveToPosition(queueTarget, deltaTime, 3); | |
} | |
moveToPosition(targetPos, deltaTime, speed) { | |
const direction = targetPos.clone().sub(this.mesh.position); | |
const distance = direction.length(); | |
if (distance > 1) { | |
direction.normalize(); | |
this.velocity.copy(direction.multiplyScalar(speed)); | |
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime)); | |
// Align rotation with movement | |
const targetAngle = Math.atan2(direction.x, direction.z); | |
this.mesh.rotation.y = targetAngle; | |
} else { | |
this.velocity.multiplyScalar(0.9); | |
} | |
} | |
leaveParking() { | |
if (!this.isParked || !this.parkingSpot) return; | |
// Free the parking spot | |
this.parkingSpot.occupied = false; | |
this.parkingSpot.car = null; | |
this.parkingSpot = null; | |
this.isParked = false; | |
this.isInApproachLane = false; | |
// Use exit lane for organized departure | |
this.useExitLane(); | |
} | |
useExitLane() { | |
// Find exit lane position | |
const exitPositions = this.targetParkingLot.exitLane; | |
let exitTarget = null; | |
for (let i = 0; i < exitPositions.length; i++) { | |
const pos = exitPositions[i]; | |
const occupied = population.some(car => | |
car !== this && | |
car.mesh.position.distanceTo(pos) < 4 | |
); | |
if (!occupied) { | |
exitTarget = pos; | |
break; | |
} | |
} | |
if (exitTarget) { | |
this.isInExitLane = true; | |
this.exitTarget = exitTarget; | |
this.mesh.position.copy(exitTarget); | |
// Set exit velocity | |
const exitDirection = new THREE.Vector3(0, 0, 1); // Move south to exit | |
this.velocity.copy(exitDirection.multiplyScalar(6)); | |
// Schedule exit lane departure | |
setTimeout(() => { | |
this.isInExitLane = false; | |
this.role = 'driver'; | |
this.timeAlive = 50 + Math.random() * 30; | |
this.updateCarColor(); | |
}, 2000); | |
} | |
} | |
attemptParking() { | |
this.role = 'parker'; | |
this.findNearestParkingLot(); | |
if (!this.targetParkingLot) { | |
// No parking available, become a wanderer | |
this.timeAlive = 20; | |
this.role = 'driver'; | |
return; | |
} | |
// Start parking process | |
this.isParkingApproach = true; | |
} | |
updateFitness(deltaTime) { | |
const distance = this.mesh.position.distanceTo(this.lastPosition); | |
this.distanceTraveled += distance; | |
// Multi-objective fitness | |
const roadBonus = this.getRoadPosition() * distance * 5; | |
const laneBonus = this.laneDiscipline * distance * 3; | |
const convoyBonus = this.convoyTime * 2; | |
const parkingBonus = this.parkingScore; | |
const violationPenalty = this.trafficViolations * -10; | |
this.fitness = this.distanceTraveled + | |
roadBonus + | |
laneBonus + | |
convoyBonus + | |
parkingBonus + | |
violationPenalty; | |
} | |
updateVisuals() { | |
this.updateCarColor(); | |
this.updateFlockVisualization(); | |
} | |
updateCarColor() { | |
let hue = 0.6; // Default blue | |
let saturation = 0.7; | |
let lightness = 0.5; | |
if (this.isParked) { | |
hue = 0.3; // Green for parked | |
lightness = 0.7; | |
} else if (this.role === 'leader') { | |
hue = 0.8; // Purple for leaders | |
saturation = 1.0; | |
lightness = 0.6; | |
} else if (this.convoyPosition >= 0) { | |
hue = 0.5; // Cyan for convoy members | |
saturation = 0.8; | |
lightness = 0.6; | |
} else if (this.getRoadPosition() < 0.3) { | |
hue = 0.1; // Orange for off-road | |
saturation = 1.0; | |
lightness = 0.5; | |
} | |
// Performance-based brightness | |
const performanceBonus = Math.min(this.fitness / 500, 0.2); | |
lightness += performanceBonus; | |
this.bodyMaterial.color.setHSL(hue, saturation, lightness); | |
} | |
updateFlockVisualization() { | |
if (!showFlockLines) return; | |
// Show convoy connections | |
let connectionIndex = 0; | |
// Leader to followers | |
if (this.role === 'leader') { | |
this.convoyFollowers.forEach(follower => { | |
if (connectionIndex < this.flockLines.length) { | |
const start = this.mesh.position.clone(); | |
start.y = 3; | |
const end = follower.mesh.position.clone(); | |
end.y = 3; | |
this.flockLines[connectionIndex].geometry.setFromPoints([start, end]); | |
this.flockLines[connectionIndex].material.color.setHex(0xff00ff); | |
this.flockLines[connectionIndex].visible = true; | |
connectionIndex++; | |
} | |
}); | |
} | |
// Following connection | |
if (this.followTarget && connectionIndex < this.flockLines.length) { | |
const start = this.mesh.position.clone(); | |
start.y = 3; | |
const end = this.followTarget.mesh.position.clone(); | |
end.y = 3; | |
this.flockLines[connectionIndex].geometry.setFromPoints([start, end]); | |
this.flockLines[connectionIndex].material.color.setHex(0x00ffff); | |
this.flockLines[connectionIndex].visible = true; | |
connectionIndex++; | |
} | |
// Neighbor connections | |
this.neighbors.slice(0, 8 - connectionIndex).forEach(neighbor => { | |
if (connectionIndex < this.flockLines.length) { | |
const start = this.mesh.position.clone(); | |
start.y = 3; | |
const end = neighbor.mesh.position.clone(); | |
end.y = 3; | |
this.flockLines[connectionIndex].geometry.setFromPoints([start, end]); | |
this.flockLines[connectionIndex].material.color.setHex(0x00ff00); | |
this.flockLines[connectionIndex].visible = true; | |
connectionIndex++; | |
} | |
}); | |
// Hide unused lines | |
for (let i = connectionIndex; i < this.flockLines.length; i++) { | |
this.flockLines[i].visible = false; | |
} | |
} | |
getObstacles() { | |
let obstacles = []; | |
population.forEach(car => { | |
if (car !== this && !car.crashed) { | |
obstacles.push(car.mesh); | |
} | |
}); | |
world.buildings.forEach(building => { | |
obstacles.push(building.mesh); | |
}); | |
return obstacles; | |
} | |
checkCollisions() { | |
const carBox = new THREE.Box3().setFromObject(this.mesh); | |
// Modified collision behavior for parking areas | |
const isParkingMode = this.isInApproachLane || this.isInExitLane || | |
this.isParkingApproach || this.role === 'parker'; | |
// Soft collision with other cars | |
population.forEach(otherCar => { | |
if (otherCar !== this && !otherCar.crashed && !otherCar.isParked) { | |
const otherBox = new THREE.Box3().setFromObject(otherCar.mesh); | |
const distance = this.mesh.position.distanceTo(otherCar.mesh.position); | |
// Relaxed collision detection for parking behavior | |
const minDistance = isParkingMode || otherCar.isInApproachLane ? 2.5 : 4.0; | |
const bothInParkingMode = isParkingMode && | |
(otherCar.isInApproachLane || otherCar.isParkingApproach || otherCar.role === 'parker'); | |
if (carBox.intersectsBox(otherBox) || (distance < minDistance && !bothInParkingMode)) { | |
// Gentle separation instead of hard collision | |
const separation = new THREE.Vector3() | |
.subVectors(this.mesh.position, otherCar.mesh.position) | |
.normalize(); | |
if (isParkingMode) { | |
// Very gentle separation for parking | |
separation.multiplyScalar(1.5); | |
this.velocity.add(separation.multiplyScalar(0.1)); | |
this.velocity.multiplyScalar(0.95); // Slow down | |
} else { | |
// Normal separation | |
separation.multiplyScalar(3); | |
this.velocity.add(separation.multiplyScalar(0.3)); | |
otherCar.velocity.sub(separation.multiplyScalar(0.3)); | |
this.fitness -= 10; | |
otherCar.fitness -= 10; | |
this.trafficViolations++; | |
} | |
} | |
} | |
}); | |
// Building collisions (unchanged) | |
world.buildings.forEach(building => { | |
const buildingBox = new THREE.Box3().setFromObject(building.mesh); | |
if (carBox.intersectsBox(buildingBox)) { | |
this.crashed = true; | |
crashCount++; | |
} | |
}); | |
} | |
keepInBounds() { | |
const bounds = 400; | |
if (Math.abs(this.mesh.position.x) > bounds || | |
Math.abs(this.mesh.position.z) > bounds) { | |
if (Math.abs(this.mesh.position.x) > bounds) { | |
this.mesh.position.x = Math.sign(this.mesh.position.x) * bounds; | |
this.velocity.x *= -0.6; | |
} | |
if (Math.abs(this.mesh.position.z) > bounds) { | |
this.mesh.position.z = Math.sign(this.mesh.position.z) * bounds; | |
this.velocity.z *= -0.6; | |
} | |
this.fitness -= 10; | |
} | |
} | |
destroy() { | |
// Clean up parking spot | |
if (this.parkingSpot) { | |
this.parkingSpot.occupied = false; | |
this.parkingSpot.car = null; | |
} | |
// Remove from parking queue | |
this.leaveParkingQueue(); | |
// Clean up visual elements | |
this.flockLines.forEach(line => { | |
if (line.parent) scene.remove(line); | |
}); | |
if (this.mesh.parent) { | |
scene.remove(this.mesh); | |
} | |
} | |
} | |
function init() { | |
// Enhanced scene setup | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x87CEEB); | |
scene.fog = new THREE.Fog(0x87CEEB, 300, 1000); | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); | |
camera.position.set(0, 150, 150); | |
camera.lookAt(0, 0, 0); | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
document.body.appendChild(renderer.domElement); | |
// Lighting | |
const ambientLight = new THREE.AmbientLight(0x404040, 0.6); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(100, 100, 50); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.width = 2048; | |
directionalLight.shadow.mapSize.height = 2048; | |
scene.add(directionalLight); | |
createTrafficWorld(); | |
createInitialPopulation(); | |
clock = new THREE.Clock(); | |
// Event listeners | |
window.addEventListener('resize', onWindowResize); | |
setupEventListeners(); | |
animate(); | |
} | |
function createTrafficWorld() { | |
// Enhanced ground with road texture hints | |
const groundGeometry = new THREE.PlaneGeometry(1200, 1200); | |
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 }); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
// Create comprehensive road network first | |
createRoadNetwork(); | |
// Then create buildings with parking lots | |
createBuildingsWithParkingLots(); | |
} | |
function createRoadNetwork() { | |
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 }); | |
const highwayMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); | |
const lineMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); | |
const yellowLineMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 }); | |
// Create comprehensive road network | |
// 1. MAJOR HIGHWAYS (8 lanes) - Cross patterns | |
createHighway(-400, 400, 0, 'horizontal', 48, highwayMaterial, yellowLineMaterial); // North highway | |
createHighway(-400, 400, 0, 'vertical', 48, highwayMaterial, yellowLineMaterial); // Main highway | |
// 2. SECONDARY HIGHWAYS (4 lanes) - Grid system | |
for (let i = -300; i <= 300; i += 150) { | |
if (i !== 0) { // Don't overlap main highway | |
createHighway(-400, 400, i, 'horizontal', 24, roadMaterial, yellowLineMaterial); | |
createHighway(-400, 400, i, 'vertical', 24, roadMaterial, yellowLineMaterial); | |
} | |
} | |
// 3. LOCAL ROADS (2 lanes) - Connecting roads | |
for (let i = -375; i <= 375; i += 75) { | |
createLocalRoad(-400, 400, i, 'horizontal', 12, roadMaterial, lineMaterial); | |
createLocalRoad(-400, 400, i, 'vertical', 12, roadMaterial, lineMaterial); | |
} | |
// 4. BUILDING ACCESS ROADS - Around all buildings | |
createBuildingAccessRoads(); | |
} | |
function createHighway(start, end, position, direction, width, material, linemat) { | |
if (direction === 'horizontal') { | |
// Main road surface | |
const roadGeometry = new THREE.PlaneGeometry(end - start, width); | |
const road = new THREE.Mesh(roadGeometry, material); | |
road.rotation.x = -Math.PI / 2; | |
road.position.set((start + end) / 2, 0.1, position); | |
scene.add(road); | |
// Lane dividers | |
const laneCount = width / 6; // 6 units per lane | |
for (let lane = 1; lane < laneCount; lane++) { | |
const lineY = position - width/2 + (lane * 6); | |
// Center divider (yellow) | |
if (lane === Math.floor(laneCount / 2)) { | |
createDashedLine(start, end, lineY, 'horizontal', linemat, true); | |
} else { | |
// Regular lane dividers (white) | |
createDashedLine(start, end, lineY, 'horizontal', linemat, false); | |
} | |
} | |
} else { | |
// Vertical highway | |
const roadGeometry = new THREE.PlaneGeometry(width, end - start); | |
const road = new THREE.Mesh(roadGeometry, material); | |
road.rotation.x = -Math.PI / 2; | |
road.position.set(position, 0.1, (start + end) / 2); | |
scene.add(road); | |
// Lane dividers | |
const laneCount = width / 6; | |
for (let lane = 1; lane < laneCount; lane++) { | |
const lineX = position - width/2 + (lane * 6); | |
if (lane === Math.floor(laneCount / 2)) { | |
createDashedLine(start, end, lineX, 'vertical', linemat, true); | |
} else { | |
createDashedLine(start, end, lineX, 'vertical', linemat, false); | |
} | |
} | |
} | |
} | |
function createLocalRoad(start, end, position, direction, width, material, linemat) { | |
if (direction === 'horizontal') { | |
const roadGeometry = new THREE.PlaneGeometry(end - start, width); | |
const road = new THREE.Mesh(roadGeometry, material); | |
road.rotation.x = -Math.PI / 2; | |
road.position.set((start + end) / 2, 0.1, position); | |
scene.add(road); | |
// Center line | |
createDashedLine(start, end, position, 'horizontal', linemat, false); | |
} else { | |
const roadGeometry = new THREE.PlaneGeometry(width, end - start); | |
const road = new THREE.Mesh(roadGeometry, material); | |
road.rotation.x = -Math.PI / 2; | |
road.position.set(position, 0.1, (start + end) / 2); | |
scene.add(road); | |
// Center line | |
createDashedLine(start, end, position, 'vertical', linemat, false); | |
} | |
} | |
function createDashedLine(start, end, position, direction, material, isDouble) { | |
const dashLength = 8; | |
const gapLength = 4; | |
const totalLength = end - start; | |
const segments = Math.floor(totalLength / (dashLength + gapLength)); | |
for (let i = 0; i < segments; i++) { | |
const segmentStart = start + i * (dashLength + gapLength); | |
if (direction === 'horizontal') { | |
const lineGeometry = new THREE.PlaneGeometry(dashLength, 0.3); | |
const line = new THREE.Mesh(lineGeometry, material); | |
line.rotation.x = -Math.PI / 2; | |
line.position.set(segmentStart + dashLength/2, 0.12, position); | |
scene.add(line); | |
// Double line for highway center | |
if (isDouble) { | |
const line2 = new THREE.Mesh(lineGeometry, material); | |
line2.rotation.x = -Math.PI / 2; | |
line2.position.set(segmentStart + dashLength/2, 0.12, position + 1); | |
scene.add(line2); | |
} | |
} else { | |
const lineGeometry = new THREE.PlaneGeometry(0.3, dashLength); | |
const line = new THREE.Mesh(lineGeometry, material); | |
line.rotation.x = -Math.PI / 2; | |
line.position.set(position, 0.12, segmentStart + dashLength/2); | |
scene.add(line); | |
if (isDouble) { | |
const line2 = new THREE.Mesh(lineGeometry, material); | |
line2.rotation.x = -Math.PI / 2; | |
line2.position.set(position + 1, 0.12, segmentStart + dashLength/2); | |
scene.add(line2); | |
} | |
} | |
} | |
} | |
function createBuildingAccessRoads() { | |
const accessRoadMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 }); | |
const lineMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); | |
world.buildings.forEach((building, index) => { | |
const pos = building.mesh.position; | |
const buildingBox = new THREE.Box3().setFromObject(building.mesh); | |
const size = buildingBox.getSize(new THREE.Vector3()); | |
// Create access roads around each building (rectangular loop) | |
const roadWidth = 8; | |
const buffer = 5; // Distance from building | |
// North road | |
createBuildingRoad( | |
pos.x - size.x/2 - buffer - 10, | |
pos.x + size.x/2 + buffer + 10, | |
pos.z + size.z/2 + buffer + roadWidth/2, | |
'horizontal', roadWidth, accessRoadMaterial, lineMaterial | |
); | |
// South road | |
createBuildingRoad( | |
pos.x - size.x/2 - buffer - 10, | |
pos.x + size.x/2 + buffer + 10, | |
pos.z - size.z/2 - buffer - roadWidth/2, | |
'horizontal', roadWidth, accessRoadMaterial, lineMaterial | |
); | |
// East road | |
createBuildingRoad( | |
pos.x + size.x/2 + buffer + roadWidth/2, | |
pos.z - size.z/2 - buffer - 10, | |
pos.z + size.z/2 + buffer + 10, | |
'vertical', roadWidth, accessRoadMaterial, lineMaterial | |
); | |
// West road | |
createBuildingRoad( | |
pos.x - size.x/2 - buffer - roadWidth/2, | |
pos.z - size.z/2 - buffer - 10, | |
pos.z + size.z/2 + buffer + 10, | |
'vertical', roadWidth, accessRoadMaterial, lineMaterial | |
); | |
// Connecting roads to main network | |
createConnectorRoads(pos, size, roadWidth, accessRoadMaterial); | |
}); | |
} | |
function createBuildingRoad(startOrX, endOrStartZ, positionOrEndZ, direction, width, material, linemat) { | |
if (direction === 'horizontal') { | |
const roadGeometry = new THREE.PlaneGeometry(endOrStartZ - startOrX, width); | |
const road = new THREE.Mesh(roadGeometry, material); | |
road.rotation.x = -Math.PI / 2; | |
road.position.set((startOrX + endOrStartZ) / 2, 0.08, positionOrEndZ); | |
scene.add(road); | |
// Center line | |
const lineGeometry = new THREE.PlaneGeometry(endOrStartZ - startOrX, 0.2); | |
const line = new THREE.Mesh(lineGeometry, linemat); | |
line.rotation.x = -Math.PI / 2; | |
line.position.set((startOrX + endOrStartZ) / 2, 0.1, positionOrEndZ); | |
scene.add(line); | |
} else { | |
const roadGeometry = new THREE.PlaneGeometry(width, positionOrEndZ - endOrStartZ); | |
const road = new THREE.Mesh(roadGeometry, material); | |
road.rotation.x = -Math.PI / 2; | |
road.position.set(startOrX, 0.08, (endOrStartZ + positionOrEndZ) / 2); | |
scene.add(road); | |
// Center line | |
const lineGeometry = new THREE.PlaneGeometry(0.2, positionOrEndZ - endOrStartZ); | |
const line = new THREE.Mesh(lineGeometry, linemat); | |
line.rotation.x = -Math.PI / 2; | |
line.position.set(startOrX, 0.1, (endOrStartZ + positionOrEndZ) / 2); | |
scene.add(line); | |
} | |
} | |
function createConnectorRoads(buildingPos, buildingSize, roadWidth, material) { | |
// Connect building access roads to nearest main roads | |
const nearestMainRoads = findNearestMainRoads(buildingPos); | |
nearestMainRoads.forEach(mainRoad => { | |
// Create connector from building to main road | |
const startPos = buildingPos.clone(); | |
const endPos = mainRoad.position.clone(); | |
// Create straight connector road | |
const distance = startPos.distanceTo(endPos); | |
const direction = endPos.clone().sub(startPos).normalize(); | |
const connectorGeometry = new THREE.PlaneGeometry(roadWidth, distance); | |
const connector = new THREE.Mesh(connectorGeometry, material); | |
connector.rotation.x = -Math.PI / 2; | |
connector.rotation.z = Math.atan2(direction.x, direction.z); | |
connector.position.copy(startPos.clone().add(endPos).multiplyScalar(0.5)); | |
connector.position.y = 0.08; | |
scene.add(connector); | |
}); | |
} | |
function findNearestMainRoads(position) { | |
const mainRoads = []; | |
// Find nearest horizontal main road | |
let nearestHorizontalDist = Infinity; | |
let nearestHorizontalZ = 0; | |
for (let z = -300; z <= 300; z += 150) { | |
const dist = Math.abs(position.z - z); | |
if (dist < nearestHorizontalDist) { | |
nearestHorizontalDist = dist; | |
nearestHorizontalZ = z; | |
} | |
} | |
if (nearestHorizontalDist < 100) { | |
mainRoads.push({ | |
position: new THREE.Vector3(position.x, 0, nearestHorizontalZ), | |
type: 'horizontal' | |
}); | |
} | |
// Find nearest vertical main road | |
let nearestVerticalDist = Infinity; | |
let nearestVerticalX = 0; | |
for (let x = -300; x <= 300; x += 150) { | |
const dist = Math.abs(position.x - x); | |
if (dist < nearestVerticalDist) { | |
nearestVerticalDist = dist; | |
nearestVerticalX = x; | |
} | |
} | |
if (nearestVerticalDist < 100) { | |
mainRoads.push({ | |
position: new THREE.Vector3(nearestVerticalX, 0, position.z), | |
type: 'vertical' | |
}); | |
} | |
return mainRoads.slice(0, 2); // Maximum 2 connections per building | |
} | |
function createBuildingsWithParkingLots() { | |
world.buildings = []; | |
world.parkingLots = []; | |
const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 }); | |
const parkingMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); | |
const spotMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); | |
const queueMaterial = new THREE.MeshLambertMaterial({ color: 0x222222 }); | |
// Create buildings at strategic locations with better spacing | |
const buildingLocations = [ | |
{ x: -200, z: -200 }, { x: 0, z: -200 }, { x: 200, z: -200 }, | |
{ x: -200, z: 0 }, { x: 200, z: 0 }, | |
{ x: -200, z: 200 }, { x: 0, z: 200 }, { x: 200, z: 200 }, | |
{ x: -100, z: -100 }, { x: 100, z: -100 }, | |
{ x: -100, z: 100 }, { x: 100, z: 100 }, | |
{ x: -300, z: -300 }, { x: 300, z: -300 }, | |
{ x: -300, z: 300 }, { x: 300, z: 300 } | |
]; | |
buildingLocations.forEach((loc, index) => { | |
// Create building | |
const width = 20 + Math.random() * 10; | |
const height = 12 + Math.random() * 20; | |
const depth = 20 + Math.random() * 10; | |
const buildingGeometry = new THREE.BoxGeometry(width, height, depth); | |
const building = new THREE.Mesh(buildingGeometry, buildingMaterial); | |
building.position.set(loc.x, height / 2, loc.z); | |
building.castShadow = true; | |
scene.add(building); | |
world.buildings.push({ mesh: building }); | |
// Create enhanced parking lot with approach lanes | |
const parkingLot = { | |
center: new THREE.Vector3(loc.x + width/2 + 25, 0.1, loc.z), | |
spots: [], | |
queue: [], | |
approachLane: [], | |
exitLane: [] | |
}; | |
// Main parking lot surface (larger) | |
const lotGeometry = new THREE.PlaneGeometry(50, 40); | |
const lot = new THREE.Mesh(lotGeometry, parkingMaterial); | |
lot.rotation.x = -Math.PI / 2; | |
lot.position.copy(parkingLot.center); | |
scene.add(lot); | |
// Approach lane (single file entry) | |
const approachGeometry = new THREE.PlaneGeometry(6, 60); | |
const approachLane = new THREE.Mesh(approachGeometry, queueMaterial); | |
approachLane.rotation.x = -Math.PI / 2; | |
approachLane.position.set(parkingLot.center.x - 30, 0.08, parkingLot.center.z); | |
scene.add(approachLane); | |
// Exit lane (single file exit) | |
const exitGeometry = new THREE.PlaneGeometry(6, 60); | |
const exitLane = new THREE.Mesh(exitGeometry, queueMaterial); | |
exitLane.rotation.x = -Math.PI / 2; | |
exitLane.position.set(parkingLot.center.x + 30, 0.08, parkingLot.center.z); | |
scene.add(exitLane); | |
// Create approach queue positions | |
for (let q = 0; q < 12; q++) { | |
const queuePos = new THREE.Vector3( | |
parkingLot.center.x - 30, | |
1, | |
parkingLot.center.z - 25 + (q * 4.5) // 4.5m spacing for tight queuing | |
); | |
parkingLot.approachLane.push(queuePos); | |
} | |
// Create exit queue positions | |
for (let q = 0; q < 8; q++) { | |
const exitPos = new THREE.Vector3( | |
parkingLot.center.x + 30, | |
1, | |
parkingLot.center.z - 15 + (q * 4) // Tighter exit spacing | |
); | |
parkingLot.exitLane.push(exitPos); | |
} | |
// Create parking spots (5x8 grid = 40 spots) | |
for (let row = 0; row < 5; row++) { | |
for (let col = 0; col < 8; col++) { | |
const spotX = parkingLot.center.x + (col - 3.5) * 6; | |
const spotZ = parkingLot.center.z + (row - 2) * 7; | |
// Spot marking | |
const spotGeometry = new THREE.PlaneGeometry(PARKING_SPOT_SIZE.width, PARKING_SPOT_SIZE.length); | |
const spotMesh = new THREE.Mesh(spotGeometry, spotMaterial); | |
spotMesh.rotation.x = -Math.PI / 2; | |
spotMesh.position.set(spotX, 0.12, spotZ); | |
scene.add(spotMesh); | |
const spot = { | |
position: new THREE.Vector3(spotX, 1, spotZ), | |
occupied: false, | |
car: null, | |
mesh: spotMesh | |
}; | |
parkingLot.spots.push(spot); | |
} | |
} | |
world.parkingLots.push(parkingLot); | |
}); | |
} | |
function createInitialPopulation() { | |
population = []; | |
for (let i = 0; i < populationSize; i++) { | |
// Start cars on roads when possible | |
const roadPositions = [ | |
{ x: -280, z: 0 }, { x: 280, z: 0 }, | |
{ x: 0, z: -280 }, { x: 0, z: 280 }, | |
{ x: -130, z: 0 }, { x: 130, z: 0 }, | |
{ x: 0, z: -130 }, { x: 0, z: 130 } | |
]; | |
const startPos = roadPositions[i % roadPositions.length]; | |
const car = new TrafficCar( | |
startPos.x + (Math.random() - 0.5) * 10, | |
startPos.z + (Math.random() - 0.5) * 10 | |
); | |
population.push(car); | |
scene.add(car.mesh); | |
} | |
} | |
function evolvePopulation() { | |
// Sort by fitness | |
population.sort((a, b) => b.fitness - a.fitness); | |
// Advanced selection | |
const eliteCount = Math.floor(populationSize * 0.15); | |
const tournamentCount = Math.floor(populationSize * 0.25); | |
const mutatedCount = populationSize - eliteCount - tournamentCount; | |
const survivors = population.slice(0, eliteCount); | |
// Tournament selection | |
for (let i = 0; i < tournamentCount; i++) { | |
const tournamentSize = 5; | |
let best = null; | |
let bestFitness = -Infinity; | |
for (let j = 0; j < tournamentSize; j++) { | |
const candidate = population[Math.floor(Math.random() * Math.min(population.length, populationSize * 0.5))]; | |
if (candidate.fitness > bestFitness) { | |
best = candidate; | |
bestFitness = candidate.fitness; | |
} | |
} | |
if (best) survivors.push(best); | |
} | |
// Clean up old population | |
population.forEach(car => car.destroy()); | |
// Create new population | |
const newPopulation = []; | |
const roadPositions = [ | |
{ x: -280, z: 0 }, { x: 280, z: 0 }, | |
{ x: 0, z: -280 }, { x: 0, z: 280 }, | |
{ x: -130, z: 0 }, { x: 130, z: 0 }, | |
{ x: 0, z: -130 }, { x: 0, z: 130 } | |
]; | |
// Elite reproduction | |
survivors.forEach((parent, index) => { | |
const startPos = roadPositions[index % roadPositions.length]; | |
const newCar = new TrafficCar( | |
startPos.x + (Math.random() - 0.5) * 10, | |
startPos.z + (Math.random() - 0.5) * 10 | |
); | |
newCar.brain = parent.brain.copy(); | |
newPopulation.push(newCar); | |
scene.add(newCar.mesh); | |
}); | |
// Mutated offspring | |
while (newPopulation.length < populationSize) { | |
const parentIndex = Math.floor(Math.random() * Math.min(survivors.length, eliteCount * 2)); | |
const parent = survivors[parentIndex]; | |
const startPos = roadPositions[newPopulation.length % roadPositions.length]; | |
const child = new TrafficCar( | |
startPos.x + (Math.random() - 0.5) * 10, | |
startPos.z + (Math.random() - 0.5) * 10 | |
); | |
child.brain = parent.brain.copy(); | |
const mutationRate = parent.fitness > bestFitness * 0.8 ? 0.05 : 0.15; | |
child.brain.mutate(mutationRate); | |
newPopulation.push(child); | |
scene.add(child.mesh); | |
} | |
population = newPopulation; | |
// Update epoch | |
epoch++; | |
timeLeft = epochTime; | |
bestFitness = Math.max(bestFitness, survivors[0]?.fitness || 0); | |
crashCount = 0; | |
parkingEvents = 0; | |
laneViolations = 0; | |
console.log(`Epoch ${epoch}: Best fitness: ${bestFitness.toFixed(1)}, Parking events: ${parkingEvents}`); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
if (!paused) { | |
const deltaTime = Math.min(clock.getDelta() * speedMultiplier, 0.1); | |
timeLeft -= deltaTime; | |
if (timeLeft <= 0) { | |
evolvePopulation(); | |
} | |
updatePopulation(deltaTime); | |
updateCamera(); | |
updateUI(); | |
} | |
renderer.render(scene, camera); | |
} | |
function updatePopulation(deltaTime) { | |
let stats = { | |
alive: 0, | |
leaders: 0, | |
convoy: 0, | |
parked: 0, | |
solo: 0, | |
maxConvoySize: 0, | |
totalRoadTime: 0, | |
totalConvoyTime: 0, | |
totalParkingScore: 0, | |
totalViolations: 0, | |
totalFollowingDistance: 0, | |
followingCount: 0, | |
approaching: 0 // Cars approaching parking | |
}; | |
population.forEach(car => { | |
car.update(deltaTime); | |
if (!car.crashed) { | |
stats.alive++; | |
stats.totalRoadTime += car.roadTime; | |
stats.totalConvoyTime += car.convoyTime; | |
stats.totalParkingScore += car.parkingScore; | |
stats.totalViolations += car.trafficViolations; | |
if (car.isParked) { | |
stats.parked++; | |
} else if (car.isParkingApproach) { | |
stats.approaching++; | |
} else if (car.role === 'leader') { | |
stats.leaders++; | |
stats.maxConvoySize = Math.max(stats.maxConvoySize, car.convoyFollowers.length + 1); | |
} else if (car.convoyPosition >= 0) { | |
stats.convoy++; | |
if (car.followingDistance > 0) { | |
stats.totalFollowingDistance += car.followingDistance; | |
stats.followingCount++; | |
} | |
} else { | |
stats.solo++; | |
} | |
} | |
}); | |
window.populationStats = stats; | |
} | |
function updateCamera() { | |
if (cameraMode === 'follow_best') { | |
// Follow best performing car | |
let bestCar = population.reduce((best, car) => { | |
if (car.crashed || car.isParked) return best; | |
return !best || car.fitness > best.fitness ? car : best; | |
}, null); | |
if (bestCar) { | |
const targetPos = bestCar.mesh.position.clone(); | |
targetPos.y += 40; | |
targetPos.add(bestCar.velocity.clone().normalize().multiplyScalar(25)); | |
camera.position.lerp(targetPos, 0.03); | |
camera.lookAt(bestCar.mesh.position); | |
} | |
} else if (cameraMode === 'follow_convoy') { | |
// Follow largest convoy | |
let largestConvoy = population.find(car => | |
car.role === 'leader' && car.convoyFollowers.length > 0 | |
); | |
if (largestConvoy) { | |
const targetPos = largestConvoy.mesh.position.clone(); | |
targetPos.y += 50; | |
targetPos.add(largestConvoy.velocity.clone().normalize().multiplyScalar(30)); | |
camera.position.lerp(targetPos, 0.03); | |
camera.lookAt(largestConvoy.mesh.position); | |
} | |
} else { | |
// Overview mode | |
camera.position.lerp(new THREE.Vector3(0, 180, 180), 0.02); | |
camera.lookAt(0, 0, 0); | |
} | |
} | |
function updateUI() { | |
const stats = window.populationStats || {}; | |
// Main UI | |
document.getElementById('epoch').textContent = epoch; | |
document.getElementById('epochTime').textContent = Math.ceil(timeLeft); | |
document.getElementById('population').textContent = stats.alive || 0; | |
document.getElementById('bestFitness').textContent = Math.round(bestFitness); | |
// Progress bar | |
const progress = ((epochTime - timeLeft) / epochTime) * 100; | |
document.getElementById('timeProgress').style.width = `${progress}%`; | |
// Traffic stats | |
if (stats.alive > 0) { | |
document.getElementById('trafficIQ').textContent = Math.round(50 + (bestFitness / 20)); | |
document.getElementById('roadMastery').textContent = Math.round((stats.totalRoadTime / stats.alive) * 10); | |
document.getElementById('laneDiscipline').textContent = Math.round(Math.max(0, 100 - (stats.totalViolations / stats.alive) * 10)); | |
document.getElementById('roadAdherence').textContent = Math.round((stats.totalRoadTime / (stats.totalRoadTime + 1)) * 100); | |
} | |
// Convoy stats | |
document.getElementById('leaderCount').textContent = stats.leaders || 0; | |
document.getElementById('convoyCount').textContent = stats.convoy || 0; | |
document.getElementById('parkedCount').textContent = stats.parked || 0; | |
document.getElementById('soloCount').textContent = stats.solo || 0; | |
document.getElementById('largestConvoy').textContent = stats.maxConvoySize || 0; | |
// Following distance | |
if (stats.followingCount > 0) { | |
document.getElementById('followingDistance').textContent = (stats.totalFollowingDistance / stats.followingCount).toFixed(1); | |
} | |
// Generation stats | |
const totalDistance = population.reduce((sum, car) => sum + car.distanceTraveled, 0); | |
const maxConvoyLength = Math.max(...population.map(car => car.convoyFollowers?.length || 0)); | |
document.getElementById('totalDistance').textContent = Math.round(totalDistance); | |
document.getElementById('parkingEvents').textContent = parkingEvents; | |
document.getElementById('laneViolations').textContent = laneViolations; | |
document.getElementById('convoyLength').textContent = maxConvoyLength; | |
document.getElementById('crashCount').textContent = crashCount; | |
// Parking stats | |
const totalSpots = world.parkingLots.reduce((sum, lot) => sum + lot.spots.length, 0); | |
const occupiedSpots = world.parkingLots.reduce((sum, lot) => | |
sum + lot.spots.filter(spot => spot.occupied).length, 0); | |
document.getElementById('spotsOccupied').textContent = occupiedSpots; | |
document.getElementById('parkingSuccess').textContent = totalSpots > 0 ? Math.round((occupiedSpots / totalSpots) * 100) : 0; | |
updateTopPerformers(); | |
} | |
function updateTopPerformers() { | |
const sorted = [...population] | |
.filter(car => !car.crashed) | |
.sort((a, b) => b.fitness - a.fitness) | |
.slice(0, 5); | |
const topPerformersDiv = document.getElementById('topPerformers'); | |
topPerformersDiv.innerHTML = ''; | |
sorted.forEach((car, i) => { | |
const div = document.createElement('div'); | |
const roleIcon = { | |
leader: 'π', | |
parker: 'π ΏοΈ', | |
driver: 'π' | |
}[car.role] || 'π'; | |
const statusIcon = car.isParked ? 'π ΏοΈ' : (car.convoyPosition >= 0 ? 'π' : 'π'); | |
div.innerHTML = `${i + 1}. ${roleIcon}${statusIcon} F:${Math.round(car.fitness)} | Lane:${Math.round(car.laneDiscipline * 100)}% | Road:${Math.round(car.roadTime)}s`; | |
div.className = car.isParked ? 'parked' : (car.role === 'leader' ? 'leader' : (car.convoyPosition >= 0 ? 'convoy' : 'solo')); | |
topPerformersDiv.appendChild(div); | |
}); | |
} | |
function setupEventListeners() { | |
document.getElementById('pauseBtn').addEventListener('click', togglePause); | |
document.getElementById('resetBtn').addEventListener('click', resetSimulation); | |
document.getElementById('speedBtn').addEventListener('click', toggleSpeed); | |
document.getElementById('viewBtn').addEventListener('click', toggleView); | |
document.getElementById('flockBtn').addEventListener('click', toggleFlockLines); | |
document.getElementById('trafficBtn').addEventListener('click', toggleTrafficRules); | |
} | |
function togglePause() { | |
paused = !paused; | |
document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause'; | |
if (!paused) clock.start(); | |
} | |
function resetSimulation() { | |
epoch = 1; | |
timeLeft = epochTime; | |
bestFitness = 0; | |
crashCount = 0; | |
parkingEvents = 0; | |
laneViolations = 0; | |
// Reset parking lots and queues | |
world.parkingLots.forEach(lot => { | |
lot.spots.forEach(spot => { | |
spot.occupied = false; | |
spot.car = null; | |
}); | |
lot.queue = []; // Clear parking queues | |
lot.approachLane = lot.approachLane || []; | |
lot.exitLane = lot.exitLane || []; | |
}); | |
population.forEach(car => car.destroy()); | |
createInitialPopulation(); | |
} | |
function toggleSpeed() { | |
speedMultiplier = speedMultiplier === 1 ? 2 : speedMultiplier === 2 ? 5 : 1; | |
document.getElementById('speedBtn').textContent = `Speed: ${speedMultiplier}x`; | |
} | |
function toggleView() { | |
const modes = ['overview', 'follow_best', 'follow_convoy']; | |
const currentIndex = modes.indexOf(cameraMode); | |
cameraMode = modes[(currentIndex + 1) % modes.length]; | |
const displayNames = { | |
overview: 'Overview', | |
follow_best: 'Follow Best', | |
follow_convoy: 'Follow Convoy' | |
}; | |
document.getElementById('viewBtn').textContent = `View: ${displayNames[cameraMode]}`; | |
} | |
function toggleFlockLines() { | |
showFlockLines = !showFlockLines; | |
document.getElementById('flockBtn').textContent = `Networks: ${showFlockLines ? 'ON' : 'OFF'}`; | |
population.forEach(car => { | |
car.flockLines.forEach(line => { | |
if (showFlockLines && !line.parent) { | |
scene.add(line); | |
} else if (!showFlockLines && line.parent) { | |
scene.remove(line); | |
} | |
}); | |
}); | |
} | |
function toggleTrafficRules() { | |
trafficRules = !trafficRules; | |
document.getElementById('trafficBtn').textContent = `Traffic Rules: ${trafficRules ? 'ON' : 'OFF'}`; | |
} | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
init(); | |
</script> | |
</body> | |
</html> |