Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Enhanced Road-Following AI 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; | |
} | |
#roadStats { | |
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; | |
} | |
#behaviorStats { | |
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; } | |
.road { color: #00aaff; } | |
.traveling { color: #ff8800; } | |
.stopped { color: #ff00ff; font-weight: bold; } | |
.navigating { color: #00ffff; } | |
.following { color: #88ff88; } | |
.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">Road-Following AI Evolution</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>Best Fitness: <span id="bestFitness">0</span></div> | |
<div>Road Mastery: <span id="roadMastery">0</span>%</div> | |
<div>Navigation IQ: <span id="navigationIQ">50</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: Follow</button> | |
<button id="pathBtn">Paths: ON</button> | |
<button id="debugBtn">Debug: OFF</button> | |
</div> | |
<div id="stats"> | |
<div><span class="highlight">Performance Metrics:</span></div> | |
<div>Destinations Reached: <span id="destinationsReached">0</span></div> | |
<div>Road Usage: <span id="roadUsage">0</span>%</div> | |
<div>Formation Quality: <span id="formationQuality">0</span>%</div> | |
<div>Path Efficiency: <span id="pathEfficiency">0</span>%</div> | |
<div>Stop Compliance: <span id="stopCompliance">0</span>%</div> | |
<div>Traffic Violations: <span id="trafficViolations">0</span></div> | |
</div> | |
<div id="roadStats"> | |
<div><span class="highlight">Road Behavior:</span></div> | |
<div><span class="road">On Roads:</span> <span id="onRoadCount">0</span></div> | |
<div><span class="traveling">Traveling:</span> <span id="travelingCount">0</span></div> | |
<div><span class="stopped">Stopped:</span> <span id="stoppedCount">0</span></div> | |
<div><span class="navigating">Navigating:</span> <span id="navigatingCount">0</span></div> | |
<div>Single File Lines: <span id="singleFileCount">0</span></div> | |
<div>Avg Line Length: <span id="avgLineLength">0</span></div> | |
<div>Traffic Flow: <span id="trafficFlow">0</span>%</div> | |
</div> | |
<div id="behaviorStats"> | |
<div><span class="highlight">AI Behaviors:</span></div> | |
<div>Path Planning: <span id="pathPlanning">0</span>%</div> | |
<div>Road Following: <span id="roadFollowing">0</span>%</div> | |
<div>Formation Control: <span id="formationControl">0</span>%</div> | |
<div>Navigation Skills: <span id="navigationSkills">0</span>%</div> | |
<div style="margin-top: 10px;"><span class="highlight">Learning Progress:</span></div> | |
<div>Exploration: <span id="explorationRate">0</span>%</div> | |
<div>Adaptation: <span id="adaptationRate">0</span>%</div> | |
<div>Specialization: <span id="specializationRate">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: [], | |
pathNetwork: new Map(), | |
roadSegments: [] | |
}; | |
// Evolution system | |
let epoch = 1; | |
let epochTime = 90; // Longer epochs for complex behaviors | |
let timeLeft = 90; | |
let population = []; | |
let populationSize = 80; // Smaller population for more complex AI | |
let bestFitness = 0; | |
let paused = false; | |
let speedMultiplier = 1; | |
let cameraMode = 'follow'; | |
let showPaths = true; | |
let debugMode = false; | |
// Road network constants | |
const ROAD_WIDTH = 12; | |
const ROAD_SPACING = 120; | |
const INTERSECTION_SIZE = 15; | |
const LANE_WIDTH = 6; | |
const BUILDING_COUNT = 16; | |
// Enhanced Neural Network for Road Navigation | |
class RoadNavigationBrain { | |
constructor() { | |
this.inputSize = 32; // Expanded for road navigation | |
this.hiddenLayers = [48, 36, 24]; // Deeper network | |
this.outputSize = 12; // More nuanced control | |
this.weights = []; | |
this.biases = []; | |
this.memory = new Array(8).fill(0); // Road memory | |
this.pathMemory = []; // Remember planned paths | |
this.initializeNetwork(); | |
// Specialized road behaviors | |
this.roadFollowingStrength = Math.random(); | |
this.formationPreference = Math.random(); | |
this.navigationSkill = Math.random(); | |
this.pathPlanningAbility = Math.random(); | |
this.stopDiscipline = Math.random(); | |
} | |
initializeNetwork() { | |
let prevSize = this.inputSize + this.memory.length; | |
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)); | |
} | |
randomMatrix(rows, cols) { | |
return Array(rows).fill().map(() => | |
Array(cols).fill().map(() => (Math.random() - 0.5) * 2) | |
); | |
} | |
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]); | |
// Update memory with important road information | |
this.updateMemory(inputs); | |
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.activationFunction(outputs[i]); | |
} | |
return outputs; | |
} | |
activationFunction(x) { | |
return Math.tanh(Math.max(-10, Math.min(10, x))); | |
} | |
updateMemory(inputs) { | |
// Store road-following information | |
const roadInfo = inputs.slice(16, 20); // Road detection inputs | |
this.memory = [...roadInfo, ...this.memory.slice(0, 4)]; | |
} | |
mutate(rate = 0.1) { | |
this.weights.forEach(matrix => { | |
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])); | |
} | |
} | |
} | |
}); | |
this.biases.forEach(bias => { | |
for (let i = 0; i < bias.length; i++) { | |
if (Math.random() < rate) { | |
bias[i] += (Math.random() - 0.5) * 0.5; | |
bias[i] = Math.max(-3, Math.min(3, bias[i])); | |
} | |
} | |
}); | |
// Mutate behavioral traits | |
if (Math.random() < rate) { | |
this.roadFollowingStrength += (Math.random() - 0.5) * 0.2; | |
this.roadFollowingStrength = Math.max(0, Math.min(1, this.roadFollowingStrength)); | |
} | |
if (Math.random() < rate) { | |
this.formationPreference += (Math.random() - 0.5) * 0.2; | |
this.formationPreference = Math.max(0, Math.min(1, this.formationPreference)); | |
} | |
} | |
copy() { | |
const newBrain = new RoadNavigationBrain(); | |
newBrain.weights = this.weights.map(matrix => matrix.map(row => [...row])); | |
newBrain.biases = this.biases.map(bias => [...bias]); | |
newBrain.memory = [...this.memory]; | |
newBrain.roadFollowingStrength = this.roadFollowingStrength; | |
newBrain.formationPreference = this.formationPreference; | |
newBrain.navigationSkill = this.navigationSkill; | |
newBrain.pathPlanningAbility = this.pathPlanningAbility; | |
newBrain.stopDiscipline = this.stopDiscipline; | |
return newBrain; | |
} | |
crossover(other) { | |
const child = new RoadNavigationBrain(); | |
for (let layer = 0; layer < this.weights.length; layer++) { | |
for (let i = 0; i < this.weights[layer].length; i++) { | |
for (let j = 0; j < this.weights[layer][i].length; j++) { | |
child.weights[layer][i][j] = Math.random() < 0.5 ? | |
this.weights[layer][i][j] : other.weights[layer][i][j]; | |
} | |
} | |
for (let i = 0; i < this.biases[layer].length; i++) { | |
child.biases[layer][i] = Math.random() < 0.5 ? | |
this.biases[layer][i] : other.biases[layer][i]; | |
} | |
} | |
// Blend behavioral traits | |
child.roadFollowingStrength = (this.roadFollowingStrength + other.roadFollowingStrength) / 2; | |
child.formationPreference = (this.formationPreference + other.formationPreference) / 2; | |
child.navigationSkill = (this.navigationSkill + other.navigationSkill) / 2; | |
child.pathPlanningAbility = (this.pathPlanningAbility + other.pathPlanningAbility) / 2; | |
child.stopDiscipline = (this.stopDiscipline + other.stopDiscipline) / 2; | |
return child; | |
} | |
} | |
// Enhanced AI Car with Road Navigation | |
class RoadNavigationCar { | |
constructor(x = 0, z = 0) { | |
this.brain = new RoadNavigationBrain(); | |
this.mesh = this.createCarMesh(); | |
this.mesh.position.set(x, 1, z); | |
// Movement properties | |
this.velocity = new THREE.Vector3(0, 0, 0); | |
this.acceleration = new THREE.Vector3(); | |
this.maxSpeed = 20; | |
this.minSpeed = 2; | |
this.targetSpeed = 15; | |
// Road navigation state | |
this.currentRoad = null; | |
this.roadPosition = 0.5; // Position on road (0-1) | |
this.roadDirection = new THREE.Vector3(1, 0, 0); | |
this.targetDestination = null; | |
this.currentPath = []; | |
this.pathIndex = 0; | |
this.state = 'seeking'; // seeking, traveling, stopped, following | |
// Formation and following | |
this.leader = null; | |
this.followers = []; | |
this.formationPosition = 0; | |
this.targetFollowingDistance = 8; | |
// Timing and stops | |
this.stopTimer = 0; | |
this.stopDuration = 10; // 10 seconds | |
this.lastDestinationTime = 0; | |
// Fitness tracking | |
this.fitness = 0; | |
this.roadUsageScore = 0; | |
this.destinationsReached = 0; | |
this.formationScore = 0; | |
this.pathEfficiencyScore = 0; | |
this.stopComplianceScore = 0; | |
this.trafficViolations = 0; | |
this.distanceTraveled = 0; | |
// Sensors | |
this.sensors = { | |
obstacles: Array(8).fill(0), | |
roads: Array(4).fill(0), | |
traffic: Array(6).fill(0), | |
destinations: Array(4).fill(0), | |
formation: Array(4).fill(0), | |
navigation: Array(6).fill(0) | |
}; | |
this.initializeNavigation(); | |
this.createVisualization(); | |
} | |
createCarMesh() { | |
const group = new THREE.Group(); | |
// Car body | |
const bodyGeometry = new THREE.BoxGeometry(1.5, 0.8, 3); | |
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); | |
// State indicator | |
this.stateIndicator = new THREE.Mesh( | |
new THREE.SphereGeometry(0.3, 8, 6), | |
new THREE.MeshLambertMaterial({ color: 0x00ff00 }) | |
); | |
this.stateIndicator.position.set(0, 1.5, 0); | |
group.add(this.stateIndicator); | |
// Direction arrow | |
const arrowGeometry = new THREE.ConeGeometry(0.2, 0.8, 6); | |
this.directionArrow = new THREE.Mesh(arrowGeometry, | |
new THREE.MeshLambertMaterial({ color: 0xffffff })); | |
this.directionArrow.position.set(0, 2, 0); | |
this.directionArrow.rotation.x = -Math.PI / 2; | |
group.add(this.directionArrow); | |
return group; | |
} | |
createVisualization() { | |
// Path visualization with proper geometry initialization | |
const pathGeometry = new THREE.BufferGeometry(); | |
// Initialize with a basic line to prevent undefined geometry | |
pathGeometry.setFromPoints([ | |
new THREE.Vector3(0, 0, 0), | |
new THREE.Vector3(0, 0, 0) | |
]); | |
this.pathLine = new THREE.Line( | |
pathGeometry, | |
new THREE.LineBasicMaterial({ | |
color: 0x00ffff, | |
transparent: true, | |
opacity: 0.6 | |
}) | |
); | |
this.pathLine.visible = showPaths; | |
scene.add(this.pathLine); | |
// Destination marker | |
this.destinationMarker = new THREE.Mesh( | |
new THREE.RingGeometry(2, 3, 8), | |
new THREE.MeshBasicMaterial({ | |
color: 0xff0000, | |
transparent: true, | |
opacity: 0.5 | |
}) | |
); | |
this.destinationMarker.rotation.x = -Math.PI / 2; | |
this.destinationMarker.visible = false; | |
scene.add(this.destinationMarker); | |
} | |
initializeNavigation() { | |
// Delay initial destination selection to ensure world is ready | |
setTimeout(() => { | |
this.selectNewDestination(); | |
this.mesh.rotation.y = Math.random() * Math.PI * 2; | |
}, 100); | |
} | |
selectNewDestination() { | |
if (!world.buildings || world.buildings.length === 0) { | |
console.warn('No buildings available for navigation'); | |
return; | |
} | |
// Choose a random building different from current position | |
let targetBuilding; | |
let attempts = 0; | |
do { | |
targetBuilding = world.buildings[Math.floor(Math.random() * world.buildings.length)]; | |
attempts++; | |
} while (this.targetDestination && | |
targetBuilding.position.distanceTo(this.targetDestination) < 50 && | |
attempts < 10); // Prevent infinite loop | |
this.targetDestination = targetBuilding.position.clone(); | |
this.planPath(); | |
this.state = 'seeking'; | |
this.lastDestinationTime = Date.now(); | |
// Update destination marker | |
if (this.destinationMarker) { | |
this.destinationMarker.position.copy(this.targetDestination); | |
this.destinationMarker.position.y = 0.1; | |
this.destinationMarker.visible = true; | |
} | |
} | |
planPath() { | |
if (!this.targetDestination) return; | |
try { | |
// Simple pathfinding using road network | |
const startPos = this.mesh.position.clone(); | |
const endPos = this.targetDestination.clone(); | |
// Find nearest road intersections | |
const startIntersection = this.findNearestIntersection(startPos); | |
const endIntersection = this.findNearestIntersection(endPos); | |
if (startIntersection && endIntersection) { | |
this.currentPath = this.findPathBetweenIntersections(startIntersection, endIntersection); | |
} else { | |
// Direct path if no road network available | |
this.currentPath = [startPos, endPos]; | |
} | |
this.pathIndex = 0; | |
this.updatePathVisualization(); | |
} catch (error) { | |
console.warn('Path planning error:', error); | |
// Fallback to direct path | |
this.currentPath = [this.mesh.position.clone(), this.targetDestination.clone()]; | |
this.pathIndex = 0; | |
} | |
} | |
findNearestIntersection(pos) { | |
let nearest = null; | |
let minDist = Infinity; | |
for (let x = -300; x <= 300; x += ROAD_SPACING) { | |
for (let z = -300; z <= 300; z += ROAD_SPACING) { | |
const intersection = new THREE.Vector3(x, 0, z); | |
const dist = pos.distanceTo(intersection); | |
if (dist < minDist) { | |
minDist = dist; | |
nearest = intersection; | |
} | |
} | |
} | |
return nearest; | |
} | |
findPathBetweenIntersections(start, end) { | |
// Simple A* pathfinding on grid | |
const path = []; | |
const current = start.clone(); | |
while (current.distanceTo(end) > ROAD_SPACING / 2) { | |
path.push(current.clone()); | |
const dx = end.x - current.x; | |
const dz = end.z - current.z; | |
if (Math.abs(dx) > Math.abs(dz)) { | |
current.x += Math.sign(dx) * ROAD_SPACING; | |
} else { | |
current.z += Math.sign(dz) * ROAD_SPACING; | |
} | |
} | |
path.push(end.clone()); | |
return path; | |
} | |
updatePathVisualization() { | |
if (!this.pathLine || !this.pathLine.geometry || !showPaths || this.currentPath.length < 2) { | |
if (this.pathLine) this.pathLine.visible = false; | |
return; | |
} | |
try { | |
const points = this.currentPath.map(p => { | |
const point = p.clone(); | |
point.y = 3; | |
return point; | |
}); | |
this.pathLine.geometry.setFromPoints(points); | |
this.pathLine.visible = true; | |
} catch (error) { | |
console.warn('Path visualization error:', error); | |
if (this.pathLine) this.pathLine.visible = false; | |
} | |
} | |
updateSensors() { | |
this.updateObstacleSensors(); | |
this.updateRoadSensors(); | |
this.updateTrafficSensors(); | |
this.updateDestinationSensors(); | |
this.updateFormationSensors(); | |
this.updateNavigationSensors(); | |
} | |
updateObstacleSensors() { | |
const directions = [ | |
[0, 0, 1], [0.7, 0, 0.7], [1, 0, 0], [0.7, 0, -0.7], | |
[0, 0, -1], [-0.7, 0, -0.7], [-1, 0, 0], [-0.7, 0, 0.7] | |
]; | |
const raycaster = new THREE.Raycaster(); | |
directions.forEach((dir, i) => { | |
const direction = new THREE.Vector3(...dir); | |
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 <= 12) { | |
this.sensors.obstacles[i] = 1 - (intersects[0].distance / 12); | |
} else { | |
this.sensors.obstacles[i] = 0; | |
} | |
}); | |
} | |
updateRoadSensors() { | |
const pos = this.mesh.position; | |
// Detect road in 4 directions | |
this.sensors.roads[0] = this.detectRoad(pos, 'north'); | |
this.sensors.roads[1] = this.detectRoad(pos, 'east'); | |
this.sensors.roads[2] = this.detectRoad(pos, 'south'); | |
this.sensors.roads[3] = this.detectRoad(pos, 'west'); | |
// Update current road status | |
this.updateCurrentRoad(); | |
} | |
detectRoad(pos, direction) { | |
const roadStrength = this.getRoadStrength(pos); | |
if (roadStrength === 0) return 0; | |
// Check road direction alignment | |
const directionVector = this.getDirectionVector(direction); | |
const roadDir = this.getRoadDirection(pos); | |
if (roadDir) { | |
const alignment = Math.abs(directionVector.dot(roadDir)); | |
return roadStrength * alignment; | |
} | |
return roadStrength; | |
} | |
getRoadStrength(pos) { | |
const roadWidth = ROAD_WIDTH; | |
// Check horizontal roads | |
const nearestHorizontalRoad = Math.round(pos.z / ROAD_SPACING) * ROAD_SPACING; | |
const distToHorizontalRoad = Math.abs(pos.z - nearestHorizontalRoad); | |
const onHorizontalRoad = distToHorizontalRoad <= roadWidth / 2; | |
// Check vertical roads | |
const nearestVerticalRoad = Math.round(pos.x / ROAD_SPACING) * ROAD_SPACING; | |
const distToVerticalRoad = Math.abs(pos.x - nearestVerticalRoad); | |
const onVerticalRoad = distToVerticalRoad <= roadWidth / 2; | |
if (onHorizontalRoad || onVerticalRoad) { | |
const hStrength = onHorizontalRoad ? 1 - (distToHorizontalRoad / (roadWidth / 2)) : 0; | |
const vStrength = onVerticalRoad ? 1 - (distToVerticalRoad / (roadWidth / 2)) : 0; | |
return Math.max(hStrength, vStrength); | |
} | |
return 0; | |
} | |
getRoadDirection(pos) { | |
const roadWidth = ROAD_WIDTH; | |
const nearestHorizontalRoad = Math.round(pos.z / ROAD_SPACING) * ROAD_SPACING; | |
const distToHorizontalRoad = Math.abs(pos.z - nearestHorizontalRoad); | |
const onHorizontalRoad = distToHorizontalRoad <= roadWidth / 2; | |
const nearestVerticalRoad = Math.round(pos.x / ROAD_SPACING) * ROAD_SPACING; | |
const distToVerticalRoad = Math.abs(pos.x - nearestVerticalRoad); | |
const onVerticalRoad = distToVerticalRoad <= roadWidth / 2; | |
if (onHorizontalRoad && onVerticalRoad) { | |
// At intersection, prefer current movement direction | |
return this.velocity.length() > 0 ? | |
this.velocity.clone().normalize() : new THREE.Vector3(1, 0, 0); | |
} else if (onHorizontalRoad) { | |
return new THREE.Vector3(1, 0, 0); | |
} else if (onVerticalRoad) { | |
return new THREE.Vector3(0, 0, 1); | |
} | |
return null; | |
} | |
getDirectionVector(direction) { | |
switch (direction) { | |
case 'north': return new THREE.Vector3(0, 0, 1); | |
case 'east': return new THREE.Vector3(1, 0, 0); | |
case 'south': return new THREE.Vector3(0, 0, -1); | |
case 'west': return new THREE.Vector3(-1, 0, 0); | |
default: return new THREE.Vector3(1, 0, 0); | |
} | |
} | |
updateCurrentRoad() { | |
const roadStrength = this.getRoadStrength(this.mesh.position); | |
const roadDir = this.getRoadDirection(this.mesh.position); | |
if (roadStrength > 0.5 && roadDir) { | |
this.currentRoad = { | |
strength: roadStrength, | |
direction: roadDir | |
}; | |
this.roadDirection = roadDir; | |
} else { | |
this.currentRoad = null; | |
} | |
} | |
updateTrafficSensors() { | |
const nearbyVehicles = population.filter(car => | |
car !== this && | |
this.mesh.position.distanceTo(car.mesh.position) < 25 | |
); | |
// Traffic in 6 directions (front, back, left, right, front-left, front-right) | |
const directions = [ | |
new THREE.Vector3(0, 0, 1), // front | |
new THREE.Vector3(0, 0, -1), // back | |
new THREE.Vector3(-1, 0, 0), // left | |
new THREE.Vector3(1, 0, 0), // right | |
new THREE.Vector3(-0.7, 0, 0.7), // front-left | |
new THREE.Vector3(0.7, 0, 0.7) // front-right | |
]; | |
directions.forEach((dir, i) => { | |
const worldDir = dir.clone().applyQuaternion(this.mesh.quaternion); | |
let trafficDensity = 0; | |
nearbyVehicles.forEach(car => { | |
const toVehicle = car.mesh.position.clone().sub(this.mesh.position).normalize(); | |
const alignment = worldDir.dot(toVehicle); | |
if (alignment > 0.5) { | |
const distance = this.mesh.position.distanceTo(car.mesh.position); | |
trafficDensity += Math.max(0, 1 - distance / 25); | |
} | |
}); | |
this.sensors.traffic[i] = Math.min(trafficDensity, 1); | |
}); | |
} | |
updateDestinationSensors() { | |
if (!this.targetDestination) { | |
this.sensors.destinations.fill(0); | |
return; | |
} | |
const toDestination = this.targetDestination.clone().sub(this.mesh.position); | |
const distance = toDestination.length(); | |
const direction = toDestination.normalize(); | |
// Convert to local coordinates | |
const localDirection = direction.clone(); | |
localDirection.applyQuaternion(this.mesh.quaternion.clone().invert()); | |
// Destination sensors: front, back, left, right | |
this.sensors.destinations[0] = Math.max(0, localDirection.z) / Math.sqrt(distance / 100 + 1); | |
this.sensors.destinations[1] = Math.max(0, -localDirection.z) / Math.sqrt(distance / 100 + 1); | |
this.sensors.destinations[2] = Math.max(0, -localDirection.x) / Math.sqrt(distance / 100 + 1); | |
this.sensors.destinations[3] = Math.max(0, localDirection.x) / Math.sqrt(distance / 100 + 1); | |
} | |
updateFormationSensors() { | |
// Single-file formation awareness | |
const nearbyVehicles = population.filter(car => | |
car !== this && | |
this.mesh.position.distanceTo(car.mesh.position) < 20 | |
); | |
let frontVehicle = null; | |
let backVehicle = null; | |
let frontDistance = Infinity; | |
let backDistance = Infinity; | |
const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion); | |
nearbyVehicles.forEach(car => { | |
const toVehicle = car.mesh.position.clone().sub(this.mesh.position); | |
const dot = forward.dot(toVehicle.normalize()); | |
const distance = toVehicle.length(); | |
if (dot > 0.8 && distance < frontDistance) { | |
frontVehicle = car; | |
frontDistance = distance; | |
} else if (dot < -0.8 && distance < backDistance) { | |
backVehicle = car; | |
backDistance = distance; | |
} | |
}); | |
this.sensors.formation[0] = frontVehicle ? Math.max(0, 1 - frontDistance / 15) : 0; | |
this.sensors.formation[1] = backVehicle ? Math.max(0, 1 - backDistance / 15) : 0; | |
this.sensors.formation[2] = nearbyVehicles.length / 10; // Local density | |
this.sensors.formation[3] = this.currentRoad ? this.currentRoad.strength : 0; | |
} | |
updateNavigationSensors() { | |
// Path following and navigation status | |
this.sensors.navigation[0] = this.currentPath.length > 0 ? 1 : 0; | |
this.sensors.navigation[1] = this.targetDestination ? | |
Math.min(1, 50 / this.mesh.position.distanceTo(this.targetDestination)) : 0; | |
this.sensors.navigation[2] = this.state === 'stopped' ? 1 : 0; | |
this.sensors.navigation[3] = this.state === 'following' ? 1 : 0; | |
this.sensors.navigation[4] = this.stopTimer / this.stopDuration; | |
this.sensors.navigation[5] = this.currentRoad ? 1 : 0; | |
} | |
update(deltaTime) { | |
this.updateSensors(); | |
this.updateBehaviorState(); | |
// Get neural network decision | |
const inputs = this.getAllInputs(); | |
const outputs = this.brain.activate(inputs); | |
// Apply movement based on neural network and current state | |
this.applyMovement(outputs, deltaTime); | |
// Update fitness | |
this.updateFitness(deltaTime); | |
// Update visuals | |
this.updateVisuals(); | |
// Update timers | |
this.updateTimers(deltaTime); | |
} | |
getAllInputs() { | |
return [ | |
...this.sensors.obstacles, // 8 inputs | |
...this.sensors.roads, // 4 inputs | |
...this.sensors.traffic, // 6 inputs | |
...this.sensors.destinations, // 4 inputs | |
...this.sensors.formation, // 4 inputs | |
...this.sensors.navigation // 6 inputs | |
// Total: 32 inputs | |
]; | |
} | |
updateBehaviorState() { | |
if (!this.targetDestination) { | |
this.selectNewDestination(); | |
return; | |
} | |
const distToDestination = this.mesh.position.distanceTo(this.targetDestination); | |
switch (this.state) { | |
case 'seeking': | |
// Look for formation opportunities | |
const nearbyLeader = this.findNearbyLeader(); | |
if (nearbyLeader && this.brain.formationPreference > 0.6) { | |
this.state = 'following'; | |
this.leader = nearbyLeader; | |
} else if (distToDestination < 8) { | |
this.state = 'stopped'; | |
this.stopTimer = this.stopDuration; | |
} | |
break; | |
case 'following': | |
if (!this.leader || this.mesh.position.distanceTo(this.leader.mesh.position) > 30) { | |
this.state = 'seeking'; | |
this.leader = null; | |
} else if (distToDestination < 8) { | |
this.state = 'stopped'; | |
this.stopTimer = this.stopDuration; | |
this.leader = null; | |
} | |
break; | |
case 'stopped': | |
if (this.stopTimer <= 0) { | |
this.selectNewDestination(); | |
this.destinationsReached++; | |
} | |
break; | |
} | |
} | |
findNearbyLeader() { | |
const candidates = population.filter(car => | |
car !== this && | |
car.state !== 'stopped' && | |
this.mesh.position.distanceTo(car.mesh.position) < 20 && | |
this.currentRoad && car.currentRoad | |
); | |
// Find car moving in similar direction on same road | |
return candidates.find(car => { | |
const alignment = this.roadDirection.dot(car.roadDirection); | |
return alignment > 0.8 || car.fitness > this.fitness; | |
}); | |
} | |
applyMovement(outputs, deltaTime) { | |
const [ | |
forward, brake, turnLeft, turnRight, | |
emergencyStop, speedUp, formationAdjust, roadFollow, | |
stopHere, pathCorrect, laneChange, precision | |
] = outputs; | |
// Emergency stop override | |
if (emergencyStop > 0.8 || this.state === 'stopped') { | |
this.velocity.multiplyScalar(0.9); | |
return; | |
} | |
// Calculate desired direction | |
let desiredDirection = new THREE.Vector3(0, 0, 1); | |
desiredDirection.applyQuaternion(this.mesh.quaternion); | |
// Road following behavior | |
if (this.currentRoad && roadFollow > 0.3) { | |
const roadInfluence = roadFollow * this.brain.roadFollowingStrength; | |
desiredDirection.lerp(this.roadDirection, roadInfluence); | |
} | |
// Formation following | |
if (this.state === 'following' && this.leader) { | |
const followDirection = this.getFollowDirection(); | |
const formationInfluence = formationAdjust * this.brain.formationPreference; | |
desiredDirection.lerp(followDirection, formationInfluence); | |
} | |
// Path following | |
if (this.currentPath.length > 0 && pathCorrect > 0.2) { | |
const pathDirection = this.getPathDirection(); | |
if (pathDirection) { | |
desiredDirection.lerp(pathDirection, pathCorrect * 0.8); | |
} | |
} | |
// Apply turning | |
const currentForward = new THREE.Vector3(0, 0, 1); | |
currentForward.applyQuaternion(this.mesh.quaternion); | |
const turnAngle = currentForward.angleTo(desiredDirection); | |
const turnDirection = currentForward.cross(desiredDirection).y; | |
if (turnAngle > 0.1) { | |
const turnAmount = Math.sign(turnDirection) * Math.min(turnAngle, 0.05) * deltaTime; | |
this.mesh.rotation.y += turnAmount; | |
} | |
// Neural network turning input | |
const neuralTurn = (turnRight - turnLeft) * 0.03 * deltaTime; | |
this.mesh.rotation.y += neuralTurn; | |
// Speed control | |
let targetSpeed = this.targetSpeed; | |
if (this.state === 'following' && this.leader) { | |
const distToLeader = this.mesh.position.distanceTo(this.leader.mesh.position); | |
if (distToLeader < this.targetFollowingDistance) { | |
targetSpeed = Math.min(targetSpeed, this.leader.velocity.length() * 0.8); | |
} | |
} | |
if (speedUp > 0.6) targetSpeed *= 1.3; | |
if (brake > 0.4) targetSpeed *= 0.5; | |
// Apply acceleration | |
const forward3d = new THREE.Vector3(0, 0, 1); | |
forward3d.applyQuaternion(this.mesh.quaternion); | |
const currentSpeed = this.velocity.length(); | |
const speedDiff = targetSpeed - currentSpeed; | |
if (forward > 0.2 && speedDiff > 0) { | |
this.acceleration.add(forward3d.multiplyScalar(speedDiff * 0.5 * deltaTime)); | |
} | |
// Apply movement | |
this.velocity.add(this.acceleration); | |
this.acceleration.multiplyScalar(0.1); // Decay | |
// Speed limits | |
if (this.velocity.length() > this.maxSpeed) { | |
this.velocity.normalize().multiplyScalar(this.maxSpeed); | |
} else if (this.velocity.length() < this.minSpeed && forward > 0.1) { | |
this.velocity.normalize().multiplyScalar(this.minSpeed); | |
} | |
// Apply position | |
const oldPosition = this.mesh.position.clone(); | |
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime)); | |
this.distanceTraveled += oldPosition.distanceTo(this.mesh.position); | |
this.keepInBounds(); | |
} | |
getFollowDirection() { | |
if (!this.leader) return new THREE.Vector3(0, 0, 1); | |
const toLeader = this.leader.mesh.position.clone().sub(this.mesh.position); | |
const distance = toLeader.length(); | |
if (distance > this.targetFollowingDistance) { | |
return toLeader.normalize(); | |
} else { | |
// Match leader's direction | |
return this.leader.velocity.clone().normalize(); | |
} | |
} | |
getPathDirection() { | |
if (this.currentPath.length === 0 || this.pathIndex >= this.currentPath.length) { | |
return null; | |
} | |
const targetWaypoint = this.currentPath[this.pathIndex]; | |
const toWaypoint = targetWaypoint.clone().sub(this.mesh.position); | |
const distance = toWaypoint.length(); | |
if (distance < 15) { | |
this.pathIndex++; | |
if (this.pathIndex < this.currentPath.length) { | |
return this.getPathDirection(); | |
} | |
} | |
return toWaypoint.normalize(); | |
} | |
updateFitness(deltaTime) { | |
const oldFitness = this.fitness; | |
// Road usage reward | |
const roadBonus = this.currentRoad ? this.currentRoad.strength * deltaTime * 100 : 0; | |
this.roadUsageScore += roadBonus; | |
// Formation reward | |
if (this.state === 'following' && this.leader) { | |
const distance = this.mesh.position.distanceTo(this.leader.mesh.position); | |
if (distance > 5 && distance < 12) { | |
this.formationScore += deltaTime * 50; | |
} | |
} | |
// Destination reaching reward | |
if (this.state === 'stopped') { | |
this.stopComplianceScore += deltaTime * 30; | |
} | |
// Path efficiency | |
if (this.currentPath.length > 0) { | |
this.pathEfficiencyScore += deltaTime * 20; | |
} | |
// Traffic violations penalty | |
const nearbyVehicles = population.filter(car => | |
car !== this && this.mesh.position.distanceTo(car.mesh.position) < 3 | |
); | |
if (nearbyVehicles.length > 0) { | |
this.trafficViolations += deltaTime; | |
} | |
// Total fitness calculation | |
this.fitness = | |
this.roadUsageScore + | |
this.formationScore + | |
this.destinationsReached * 200 + | |
this.pathEfficiencyScore + | |
this.stopComplianceScore + | |
this.distanceTraveled * 0.5 - | |
this.trafficViolations * 50; | |
// Update global best | |
if (this.fitness > bestFitness) { | |
bestFitness = this.fitness; | |
} | |
} | |
updateVisuals() { | |
// State color coding | |
const stateColors = { | |
seeking: 0x00ff00, | |
following: 0x0088ff, | |
stopped: 0xff0000, | |
traveling: 0xffff00 | |
}; | |
this.stateIndicator.material.color.setHex(stateColors[this.state] || 0xffffff); | |
// Direction arrow points toward target or leader | |
if (this.state === 'following' && this.leader) { | |
const toLeader = this.leader.mesh.position.clone().sub(this.mesh.position); | |
this.directionArrow.lookAt(this.mesh.position.clone().add(toLeader)); | |
} else if (this.targetDestination) { | |
this.directionArrow.lookAt(this.targetDestination); | |
} | |
// Body color based on performance | |
const performance = Math.min(this.fitness / 1000, 1); | |
this.bodyMaterial.color.setHSL( | |
performance * 0.3, // Green to red spectrum | |
0.8, | |
0.4 + performance * 0.4 | |
); | |
} | |
updateTimers(deltaTime) { | |
if (this.state === 'stopped') { | |
this.stopTimer -= deltaTime; | |
} | |
} | |
getObstacles() { | |
const obstacles = []; | |
try { | |
if (population) { | |
population.forEach(car => { | |
if (car !== this && car.mesh) obstacles.push(car.mesh); | |
}); | |
} | |
if (world.buildings) { | |
world.buildings.forEach(building => { | |
if (building.mesh) obstacles.push(building.mesh); | |
}); | |
} | |
} catch (error) { | |
console.warn('Error getting obstacles:', error); | |
} | |
return obstacles; | |
} | |
keepInBounds() { | |
const bounds = 350; | |
if (Math.abs(this.mesh.position.x) > bounds) { | |
this.mesh.position.x = Math.sign(this.mesh.position.x) * bounds; | |
this.velocity.x *= -0.5; | |
} | |
if (Math.abs(this.mesh.position.z) > bounds) { | |
this.mesh.position.z = Math.sign(this.mesh.position.z) * bounds; | |
this.velocity.z *= -0.5; | |
} | |
} | |
destroy() { | |
try { | |
if (this.pathLine && this.pathLine.parent) { | |
scene.remove(this.pathLine); | |
if (this.pathLine.geometry) this.pathLine.geometry.dispose(); | |
if (this.pathLine.material) this.pathLine.material.dispose(); | |
} | |
if (this.destinationMarker && this.destinationMarker.parent) { | |
scene.remove(this.destinationMarker); | |
if (this.destinationMarker.geometry) this.destinationMarker.geometry.dispose(); | |
if (this.destinationMarker.material) this.destinationMarker.material.dispose(); | |
} | |
if (this.mesh && this.mesh.parent) { | |
scene.remove(this.mesh); | |
// Dispose of car mesh materials and geometries | |
this.mesh.traverse((child) => { | |
if (child.geometry) child.geometry.dispose(); | |
if (child.material) { | |
if (Array.isArray(child.material)) { | |
child.material.forEach(mat => mat.dispose()); | |
} else { | |
child.material.dispose(); | |
} | |
} | |
}); | |
} | |
} catch (error) { | |
console.warn('Cleanup error:', error); | |
} | |
} | |
} | |
function init() { | |
// Scene setup | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x87CEEB); | |
scene.fog = new THREE.Fog(0x87CEEB, 200, 800); | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.set(0, 80, 80); | |
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; | |
renderer.setClearColor(0x001122); | |
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(50, 100, 50); | |
directionalLight.castShadow = true; | |
scene.add(directionalLight); | |
createWorld(); | |
createPopulation(); | |
clock = new THREE.Clock(); | |
window.addEventListener('resize', onWindowResize); | |
setupControls(); | |
animate(); | |
} | |
function createWorld() { | |
// Ground | |
const groundGeometry = new THREE.PlaneGeometry(800, 800); | |
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 }); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
createRoadNetwork(); | |
createBuildings(); | |
} | |
function createRoadNetwork() { | |
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 }); | |
// Create grid of roads | |
for (let x = -300; x <= 300; x += ROAD_SPACING) { | |
// Vertical road | |
const vRoadGeometry = new THREE.PlaneGeometry(ROAD_WIDTH, 600); | |
const vRoad = new THREE.Mesh(vRoadGeometry, roadMaterial); | |
vRoad.rotation.x = -Math.PI / 2; | |
vRoad.position.set(x, 0.1, 0); | |
scene.add(vRoad); | |
// Lane dividers | |
for (let z = -290; z <= 290; z += 20) { | |
const dividerGeometry = new THREE.PlaneGeometry(0.5, 8); | |
const dividerMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 }); | |
const divider = new THREE.Mesh(dividerGeometry, dividerMaterial); | |
divider.rotation.x = -Math.PI / 2; | |
divider.position.set(x, 0.2, z); | |
scene.add(divider); | |
} | |
} | |
for (let z = -300; z <= 300; z += ROAD_SPACING) { | |
// Horizontal road | |
const hRoadGeometry = new THREE.PlaneGeometry(600, ROAD_WIDTH); | |
const hRoad = new THREE.Mesh(hRoadGeometry, roadMaterial); | |
hRoad.rotation.x = -Math.PI / 2; | |
hRoad.position.set(0, 0.1, z); | |
scene.add(hRoad); | |
// Lane dividers | |
for (let x = -290; x <= 290; x += 20) { | |
const dividerGeometry = new THREE.PlaneGeometry(8, 0.5); | |
const dividerMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 }); | |
const divider = new THREE.Mesh(dividerGeometry, dividerMaterial); | |
divider.rotation.x = -Math.PI / 2; | |
divider.position.set(x, 0.2, z); | |
scene.add(divider); | |
} | |
} | |
} | |
function createBuildings() { | |
world.buildings = []; | |
const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 }); | |
// Place buildings at strategic locations away from roads | |
const buildingPositions = [ | |
[-180, -180], [-60, -180], [60, -180], [180, -180], | |
[-180, -60], [-60, -60], [60, -60], [180, -60], | |
[-180, 60], [-60, 60], [60, 60], [180, 60], | |
[-180, 180], [-60, 180], [60, 180], [180, 180] | |
]; | |
buildingPositions.forEach(([x, z]) => { | |
const width = 20 + Math.random() * 15; | |
const height = 10 + Math.random() * 25; | |
const depth = 20 + Math.random() * 15; | |
const buildingGeometry = new THREE.BoxGeometry(width, height, depth); | |
const building = new THREE.Mesh(buildingGeometry, buildingMaterial); | |
building.position.set(x, height / 2, z); | |
building.castShadow = true; | |
scene.add(building); | |
world.buildings.push({ | |
mesh: building, | |
position: new THREE.Vector3(x, 0, z) | |
}); | |
}); | |
} | |
function createPopulation() { | |
population = []; | |
// Start cars at various intersections | |
const startPositions = [ | |
[-120, -120], [0, -120], [120, -120], | |
[-120, 0], [0, 0], [120, 0], | |
[-120, 120], [0, 120], [120, 120] | |
]; | |
for (let i = 0; i < populationSize; i++) { | |
const startPos = startPositions[i % startPositions.length]; | |
const x = startPos[0] + (Math.random() - 0.5) * 20; | |
const z = startPos[1] + (Math.random() - 0.5) * 20; | |
const car = new RoadNavigationCar(x, z); | |
population.push(car); | |
scene.add(car.mesh); | |
} | |
} | |
function evolve() { | |
console.log(`Epoch ${epoch} complete. Best fitness: ${bestFitness.toFixed(1)}`); | |
// Sort population by fitness | |
population.sort((a, b) => b.fitness - a.fitness); | |
// Keep top performers | |
const eliteCount = Math.floor(populationSize * 0.3); | |
const newPopulation = []; | |
// Elite selection | |
for (let i = 0; i < eliteCount; i++) { | |
const elite = population[i]; | |
const newCar = createNewCar(); | |
newCar.brain = elite.brain.copy(); | |
newPopulation.push(newCar); | |
} | |
// Crossover and mutation | |
while (newPopulation.length < populationSize) { | |
const parent1 = tournamentSelection(); | |
const parent2 = tournamentSelection(); | |
const child = createNewCar(); | |
child.brain = parent1.brain.crossover(parent2.brain); | |
child.brain.mutate(0.1); | |
newPopulation.push(child); | |
} | |
// Replace population | |
population.forEach(car => car.destroy()); | |
population = newPopulation; | |
population.forEach(car => scene.add(car.mesh)); | |
// Reset for new epoch | |
epoch++; | |
timeLeft = epochTime; | |
} | |
function createNewCar() { | |
const startPositions = [ | |
[-120, -120], [0, -120], [120, -120], | |
[-120, 0], [0, 0], [120, 0], | |
[-120, 120], [0, 120], [120, 120] | |
]; | |
const startPos = startPositions[Math.floor(Math.random() * startPositions.length)]; | |
const x = startPos[0] + (Math.random() - 0.5) * 20; | |
const z = startPos[1] + (Math.random() - 0.5) * 20; | |
return new RoadNavigationCar(x, z); | |
} | |
function tournamentSelection(tournamentSize = 3) { | |
let best = null; | |
let bestFitness = -Infinity; | |
for (let i = 0; i < tournamentSize; i++) { | |
const candidate = population[Math.floor(Math.random() * population.length)]; | |
if (candidate.fitness > bestFitness) { | |
best = candidate; | |
bestFitness = candidate.fitness; | |
} | |
} | |
return best; | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
if (!paused) { | |
const deltaTime = Math.min(clock.getDelta() * speedMultiplier, 0.05); | |
timeLeft -= deltaTime; | |
if (timeLeft <= 0) { | |
evolve(); | |
} | |
updatePopulation(deltaTime); | |
updateCamera(); | |
updateUI(); | |
} | |
renderer.render(scene, camera); | |
} | |
function updatePopulation(deltaTime) { | |
population.forEach(car => car.update(deltaTime)); | |
} | |
function updateCamera() { | |
if (cameraMode === 'follow') { | |
const bestCar = population.reduce((best, car) => | |
car.fitness > best.fitness ? car : best | |
); | |
const targetPos = bestCar.mesh.position.clone(); | |
targetPos.y += 40; | |
targetPos.add(bestCar.velocity.clone().normalize().multiplyScalar(20)); | |
camera.position.lerp(targetPos, 0.02); | |
camera.lookAt(bestCar.mesh.position); | |
} else { | |
camera.position.lerp(new THREE.Vector3(0, 150, 150), 0.02); | |
camera.lookAt(0, 0, 0); | |
} | |
} | |
function updateUI() { | |
// Basic stats | |
document.getElementById('epoch').textContent = epoch; | |
document.getElementById('epochTime').textContent = Math.ceil(timeLeft); | |
document.getElementById('population').textContent = population.length; | |
document.getElementById('bestFitness').textContent = Math.round(bestFitness); | |
// Progress bar | |
const progress = ((epochTime - timeLeft) / epochTime) * 100; | |
document.getElementById('timeProgress').style.width = `${progress}%`; | |
// Calculate aggregate stats | |
let stats = { | |
onRoad: 0, | |
traveling: 0, | |
stopped: 0, | |
navigating: 0, | |
totalDestinations: 0, | |
totalRoadUsage: 0, | |
totalFormation: 0, | |
totalPathEfficiency: 0, | |
totalStopCompliance: 0, | |
totalViolations: 0, | |
singleFileLines: 0, | |
totalLineLength: 0 | |
}; | |
population.forEach(car => { | |
if (car.currentRoad) stats.onRoad++; | |
if (car.state === 'seeking') stats.traveling++; | |
if (car.state === 'stopped') stats.stopped++; | |
if (car.currentPath.length > 0) stats.navigating++; | |
stats.totalDestinations += car.destinationsReached; | |
stats.totalRoadUsage += car.roadUsageScore; | |
stats.totalFormation += car.formationScore; | |
stats.totalPathEfficiency += car.pathEfficiencyScore; | |
stats.totalStopCompliance += car.stopComplianceScore; | |
stats.totalViolations += car.trafficViolations; | |
}); | |
// Update UI elements | |
document.getElementById('onRoadCount').textContent = stats.onRoad; | |
document.getElementById('travelingCount').textContent = stats.traveling; | |
document.getElementById('stoppedCount').textContent = stats.stopped; | |
document.getElementById('navigatingCount').textContent = stats.navigating; | |
document.getElementById('destinationsReached').textContent = stats.totalDestinations; | |
document.getElementById('roadUsage').textContent = Math.round((stats.totalRoadUsage / population.length) / 10); | |
document.getElementById('formationQuality').textContent = Math.round((stats.totalFormation / population.length) / 10); | |
document.getElementById('pathEfficiency').textContent = Math.round((stats.totalPathEfficiency / population.length) / 10); | |
document.getElementById('stopCompliance').textContent = Math.round((stats.totalStopCompliance / population.length) / 10); | |
document.getElementById('trafficViolations').textContent = Math.round(stats.totalViolations); | |
// Behavioral stats | |
const avgRoadFollowing = population.reduce((sum, car) => sum + car.brain.roadFollowingStrength, 0) / population.length; | |
const avgFormationPref = population.reduce((sum, car) => sum + car.brain.formationPreference, 0) / population.length; | |
const avgNavSkill = population.reduce((sum, car) => sum + car.brain.navigationSkill, 0) / population.length; | |
document.getElementById('roadFollowing').textContent = Math.round(avgRoadFollowing * 100); | |
document.getElementById('formationControl').textContent = Math.round(avgFormationPref * 100); | |
document.getElementById('navigationSkills').textContent = Math.round(avgNavSkill * 100); | |
document.getElementById('navigationIQ').textContent = Math.round(avgNavSkill * 100); | |
document.getElementById('roadMastery').textContent = Math.round(avgRoadFollowing * 100); | |
} | |
function setupControls() { | |
document.getElementById('pauseBtn').addEventListener('click', () => { | |
paused = !paused; | |
document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause'; | |
}); | |
document.getElementById('resetBtn').addEventListener('click', () => { | |
population.forEach(car => car.destroy()); | |
epoch = 1; | |
timeLeft = epochTime; | |
bestFitness = 0; | |
createPopulation(); | |
}); | |
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', () => { | |
cameraMode = cameraMode === 'follow' ? 'overview' : 'follow'; | |
document.getElementById('viewBtn').textContent = `View: ${cameraMode === 'follow' ? 'Follow' : 'Overview'}`; | |
}); | |
document.getElementById('pathBtn').addEventListener('click', () => { | |
showPaths = !showPaths; | |
document.getElementById('pathBtn').textContent = `Paths: ${showPaths ? 'ON' : 'OFF'}`; | |
population.forEach(car => { | |
if (car.pathLine) { | |
car.pathLine.visible = showPaths; | |
if (showPaths) { | |
car.updatePathVisualization(); | |
} | |
} | |
}); | |
}); | |
document.getElementById('debugBtn').addEventListener('click', () => { | |
debugMode = !debugMode; | |
document.getElementById('debugBtn').textContent = `Debug: ${debugMode ? 'ON' : 'OFF'}`; | |
}); | |
} | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
init(); | |
</script> | |
</body> | |
</html> |