|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Embodied AI - Autonomous Vehicle Simulator</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
min-height: 100vh; |
|
display: flex; |
|
flex-direction: column; |
|
color: white; |
|
} |
|
|
|
.header { |
|
background: rgba(0, 0, 0, 0.3); |
|
padding: 20px; |
|
text-align: center; |
|
backdrop-filter: blur(10px); |
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
|
} |
|
|
|
.header h1 { |
|
font-size: 2.5rem; |
|
margin-bottom: 10px; |
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
.header p { |
|
font-size: 1.1rem; |
|
opacity: 0.9; |
|
} |
|
|
|
.main-container { |
|
display: flex; |
|
flex: 1; |
|
gap: 20px; |
|
padding: 20px; |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
width: 100%; |
|
} |
|
|
|
.simulation-area { |
|
flex: 1; |
|
background: rgba(255, 255, 255, 0.1); |
|
border-radius: 15px; |
|
padding: 20px; |
|
backdrop-filter: blur(10px); |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
} |
|
|
|
.canvas-container { |
|
position: relative; |
|
width: 100%; |
|
height: 600px; |
|
background: #1a1a2e; |
|
border-radius: 10px; |
|
overflow: hidden; |
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
#simulationCanvas { |
|
width: 100%; |
|
height: 100%; |
|
display: block; |
|
} |
|
|
|
.controls-panel { |
|
width: 300px; |
|
background: rgba(255, 255, 255, 0.1); |
|
border-radius: 15px; |
|
padding: 20px; |
|
backdrop-filter: blur(10px); |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
height: fit-content; |
|
} |
|
|
|
.control-section { |
|
margin-bottom: 25px; |
|
} |
|
|
|
.control-section h3 { |
|
font-size: 1.2rem; |
|
margin-bottom: 15px; |
|
color: #fff; |
|
border-bottom: 2px solid rgba(255, 255, 255, 0.3); |
|
padding-bottom: 8px; |
|
} |
|
|
|
.control-group { |
|
margin-bottom: 15px; |
|
} |
|
|
|
.control-group label { |
|
display: block; |
|
margin-bottom: 5px; |
|
font-weight: 500; |
|
color: #e0e0e0; |
|
} |
|
|
|
.control-group input, .control-group select { |
|
width: 100%; |
|
padding: 8px 12px; |
|
border: none; |
|
border-radius: 8px; |
|
background: rgba(255, 255, 255, 0.2); |
|
color: white; |
|
font-size: 14px; |
|
} |
|
|
|
.control-group input::placeholder { |
|
color: rgba(255, 255, 255, 0.6); |
|
} |
|
|
|
.btn { |
|
width: 100%; |
|
padding: 12px; |
|
border: none; |
|
border-radius: 8px; |
|
font-size: 16px; |
|
font-weight: 600; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.btn-primary { |
|
background: linear-gradient(45deg, #4CAF50, #45a049); |
|
color: white; |
|
} |
|
|
|
.btn-secondary { |
|
background: linear-gradient(45deg, #2196F3, #1976D2); |
|
color: white; |
|
} |
|
|
|
.btn-danger { |
|
background: linear-gradient(45deg, #f44336, #d32f2f); |
|
color: white; |
|
} |
|
|
|
.btn:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
.stats { |
|
background: rgba(0, 0, 0, 0.3); |
|
padding: 15px; |
|
border-radius: 8px; |
|
margin-top: 15px; |
|
} |
|
|
|
.stats h4 { |
|
margin-bottom: 10px; |
|
color: #fff; |
|
} |
|
|
|
.stat-item { |
|
display: flex; |
|
justify-content: space-between; |
|
margin-bottom: 8px; |
|
font-size: 14px; |
|
} |
|
|
|
.stat-label { |
|
color: #ccc; |
|
} |
|
|
|
.stat-value { |
|
color: #4CAF50; |
|
font-weight: 600; |
|
} |
|
|
|
.ai-info { |
|
background: rgba(0, 0, 0, 0.3); |
|
padding: 15px; |
|
border-radius: 8px; |
|
margin-top: 15px; |
|
font-size: 13px; |
|
line-height: 1.4; |
|
} |
|
|
|
.legend { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 15px; |
|
margin-top: 15px; |
|
} |
|
|
|
.legend-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
font-size: 12px; |
|
} |
|
|
|
.legend-color { |
|
width: 16px; |
|
height: 16px; |
|
border-radius: 50%; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.main-container { |
|
flex-direction: column; |
|
} |
|
|
|
.controls-panel { |
|
width: 100%; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="header"> |
|
<h1>🚗 Embodied AI Vehicle Simulator</h1> |
|
<p>Interactive autonomous vehicle navigation with real-time AI decision making</p> |
|
</div> |
|
|
|
<div class="main-container"> |
|
<div class="simulation-area"> |
|
<div class="canvas-container"> |
|
<canvas id="simulationCanvas"></canvas> |
|
</div> |
|
|
|
<div class="legend"> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background: #4CAF50;"></div> |
|
<span>Autonomous Vehicle</span> |
|
</div> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background: #f44336;"></div> |
|
<span>Obstacles</span> |
|
</div> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background: #2196F3;"></div> |
|
<span>Target Destinations</span> |
|
</div> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background: #FFC107;"></div> |
|
<span>Sensors (LiDAR)</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="controls-panel"> |
|
<div class="control-section"> |
|
<h3>🎮 Simulation Controls</h3> |
|
<button class="btn btn-primary" id="startBtn">Start Simulation</button> |
|
<button class="btn btn-secondary" id="pauseBtn">Pause</button> |
|
<button class="btn btn-danger" id="resetBtn">Reset</button> |
|
</div> |
|
|
|
<div class="control-section"> |
|
<h3>⚙️ Parameters</h3> |
|
<div class="control-group"> |
|
<label>Vehicle Speed</label> |
|
<input type="range" id="speedSlider" min="0" max="5" value="2" step="0.5"> |
|
<span id="speedValue">2.0</span> |
|
</div> |
|
<div class="control-group"> |
|
<label>Sensor Range</label> |
|
<input type="range" id="sensorRange" min="50" max="200" value="100" step="10"> |
|
<span id="sensorValue">100</span> |
|
</div> |
|
<div class="control-group"> |
|
<label>AI Behavior</label> |
|
<select id="behaviorSelect"> |
|
<option value="cautious">Cautious</option> |
|
<option value="normal" selected>Normal</option> |
|
<option value="aggressive">Aggressive</option> |
|
</select> |
|
</div> |
|
</div> |
|
|
|
<div class="control-section"> |
|
<h3>🏭 Environment</h3> |
|
<button class="btn btn-secondary" id="addObstacleBtn">Add Obstacle</button> |
|
<button class="btn btn-secondary" id="addTargetBtn">Add Target</button> |
|
<button class="btn btn-danger" id="clearBtn">Clear All</button> |
|
</div> |
|
|
|
<div class="stats"> |
|
<h4>📊 Real-time Stats</h4> |
|
<div class="stat-item"> |
|
<span class="stat-label">Distance Traveled:</span> |
|
<span class="stat-value" id="distanceTraveled">0m</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">Targets Reached:</span> |
|
<span class="stat-value" id="targetsReached">0</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">Obstacles Avoided:</span> |
|
<span class="stat-value" id="obstaclesAvoided">0</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">AI Decisions/sec:</span> |
|
<span class="stat-value" id="decisionsPerSec">0</span> |
|
</div> |
|
</div> |
|
|
|
<div class="ai-info"> |
|
<h4>🧠 AI Decision Making</h4> |
|
<p id="currentDecision">System initializing...</p> |
|
<br> |
|
<p><strong>Embodied AI Features:</strong></p> |
|
<ul style="margin-left: 15px; margin-top: 5px;"> |
|
<li>Real-time sensor processing</li> |
|
<li>Dynamic path planning</li> |
|
<li>Obstacle avoidance</li> |
|
<li>Goal-oriented behavior</li> |
|
<li>Environmental adaptation</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const canvas = document.getElementById('simulationCanvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
function resizeCanvas() { |
|
const container = canvas.parentElement; |
|
canvas.width = container.clientWidth; |
|
canvas.height = container.clientHeight; |
|
} |
|
|
|
resizeCanvas(); |
|
window.addEventListener('resize', resizeCanvas); |
|
|
|
|
|
let isRunning = false; |
|
let animationId = null; |
|
let lastTime = 0; |
|
let decisionCount = 0; |
|
let lastDecisionTime = 0; |
|
|
|
|
|
class AutonomousVehicle { |
|
constructor(x, y) { |
|
this.x = x; |
|
this.y = y; |
|
this.vx = 0; |
|
this.vy = 0; |
|
this.angle = 0; |
|
this.speed = 2; |
|
this.maxSpeed = 5; |
|
this.size = 15; |
|
this.sensorRange = 100; |
|
this.sensors = []; |
|
this.path = []; |
|
this.currentTarget = null; |
|
this.distanceTraveled = 0; |
|
this.lastX = x; |
|
this.lastY = y; |
|
this.behavior = 'normal'; |
|
this.avoidanceVector = { x: 0, y: 0 }; |
|
} |
|
|
|
updateSensors() { |
|
this.sensors = []; |
|
const numSensors = 8; |
|
|
|
for (let i = 0; i < numSensors; i++) { |
|
const angle = (i / numSensors) * 2 * Math.PI; |
|
const endX = this.x + Math.cos(angle) * this.sensorRange; |
|
const endY = this.y + Math.sin(angle) * this.sensorRange; |
|
|
|
let distance = this.sensorRange; |
|
|
|
|
|
for (let obstacle of obstacles) { |
|
const dx = obstacle.x - this.x; |
|
const dy = obstacle.y - this.y; |
|
const distToObstacle = Math.sqrt(dx * dx + dy * dy); |
|
|
|
if (distToObstacle < distance && distToObstacle > 0) { |
|
const angleToObstacle = Math.atan2(dy, dx); |
|
const angleDiff = Math.abs(angle - angleToObstacle); |
|
|
|
if (angleDiff < 0.5 || angleDiff > 2 * Math.PI - 0.5) { |
|
distance = Math.max(0, distToObstacle - obstacle.size); |
|
} |
|
} |
|
} |
|
|
|
this.sensors.push({ |
|
angle: angle, |
|
distance: distance, |
|
x: this.x + Math.cos(angle) * distance, |
|
y: this.y + Math.sin(angle) * distance |
|
}); |
|
} |
|
} |
|
|
|
makeDecision() { |
|
decisionCount++; |
|
|
|
|
|
let closestTarget = null; |
|
let minDistance = Infinity; |
|
|
|
for (let target of targets) { |
|
const dx = target.x - this.x; |
|
const dy = target.y - this.y; |
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
if (distance < minDistance) { |
|
minDistance = distance; |
|
closestTarget = target; |
|
} |
|
} |
|
|
|
this.currentTarget = closestTarget; |
|
|
|
|
|
this.avoidanceVector = { x: 0, y: 0 }; |
|
|
|
for (let sensor of this.sensors) { |
|
if (sensor.distance < this.sensorRange * 0.7) { |
|
const avoidanceStrength = (this.sensorRange * 0.7 - sensor.distance) / (this.sensorRange * 0.7); |
|
this.avoidanceVector.x -= Math.cos(sensor.angle) * avoidanceStrength; |
|
this.avoidanceVector.y -= Math.sin(sensor.angle) * avoidanceStrength; |
|
} |
|
} |
|
|
|
|
|
const avoidanceMag = Math.sqrt(this.avoidanceVector.x ** 2 + this.avoidanceVector.y ** 2); |
|
if (avoidanceMag > 0) { |
|
this.avoidanceVector.x /= avoidanceMag; |
|
this.avoidanceVector.y /= avoidanceMag; |
|
} |
|
|
|
|
|
const decisionElement = document.getElementById('currentDecision'); |
|
if (this.currentTarget) { |
|
if (avoidanceMag > 0.1) { |
|
decisionElement.textContent = `Avoiding obstacles while navigating to target (${Math.round(minDistance)}m away)`; |
|
} else { |
|
decisionElement.textContent = `Direct path to target (${Math.round(minDistance)}m away)`; |
|
} |
|
} else { |
|
decisionElement.textContent = 'Searching for targets...'; |
|
} |
|
} |
|
|
|
update(deltaTime) { |
|
this.updateSensors(); |
|
this.makeDecision(); |
|
|
|
|
|
let targetVector = { x: 0, y: 0 }; |
|
if (this.currentTarget) { |
|
const dx = this.currentTarget.x - this.x; |
|
const dy = this.currentTarget.y - this.y; |
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
if (distance > 0) { |
|
targetVector.x = dx / distance; |
|
targetVector.y = dy / distance; |
|
} |
|
|
|
|
|
if (distance < 20) { |
|
const index = targets.indexOf(this.currentTarget); |
|
if (index > -1) { |
|
targets.splice(index, 1); |
|
stats.targetsReached++; |
|
document.getElementById('targetsReached').textContent = stats.targetsReached; |
|
} |
|
} |
|
} |
|
|
|
|
|
let finalVector = { x: 0, y: 0 }; |
|
const behaviorSettings = { |
|
cautious: { avoidance: 0.8, target: 0.2 }, |
|
normal: { avoidance: 0.6, target: 0.4 }, |
|
aggressive: { avoidance: 0.3, target: 0.7 } |
|
}; |
|
|
|
const settings = behaviorSettings[this.behavior]; |
|
finalVector.x = targetVector.x * settings.target + this.avoidanceVector.x * settings.avoidance; |
|
finalVector.y = targetVector.y * settings.target + this.avoidanceVector.y * settings.avoidance; |
|
|
|
|
|
const finalMag = Math.sqrt(finalVector.x ** 2 + finalVector.y ** 2); |
|
if (finalMag > 0) { |
|
finalVector.x /= finalMag; |
|
finalVector.y /= finalMag; |
|
} |
|
|
|
|
|
this.vx = finalVector.x * this.speed; |
|
this.vy = finalVector.y * this.speed; |
|
|
|
|
|
this.x += this.vx * deltaTime; |
|
this.y += this.vy * deltaTime; |
|
|
|
|
|
this.x = Math.max(this.size, Math.min(canvas.width - this.size, this.x)); |
|
this.y = Math.max(this.size, Math.min(canvas.height - this.size, this.y)); |
|
|
|
|
|
if (this.vx !== 0 || this.vy !== 0) { |
|
this.angle = Math.atan2(this.vy, this.vx); |
|
} |
|
|
|
|
|
const dx = this.x - this.lastX; |
|
const dy = this.y - this.lastY; |
|
this.distanceTraveled += Math.sqrt(dx * dx + dy * dy); |
|
this.lastX = this.x; |
|
this.lastY = this.y; |
|
} |
|
|
|
draw() { |
|
|
|
ctx.strokeStyle = 'rgba(255, 193, 7, 0.3)'; |
|
ctx.lineWidth = 1; |
|
for (let sensor of this.sensors) { |
|
ctx.beginPath(); |
|
ctx.moveTo(this.x, this.y); |
|
ctx.lineTo(sensor.x, sensor.y); |
|
ctx.stroke(); |
|
} |
|
|
|
|
|
ctx.save(); |
|
ctx.translate(this.x, this.y); |
|
ctx.rotate(this.angle); |
|
|
|
|
|
ctx.fillStyle = '#4CAF50'; |
|
ctx.fillRect(-this.size, -this.size/2, this.size * 2, this.size); |
|
|
|
|
|
ctx.fillStyle = '#fff'; |
|
ctx.fillRect(this.size/2, -this.size/4, this.size/2, this.size/2); |
|
|
|
ctx.restore(); |
|
|
|
|
|
if (this.path.length > 1) { |
|
ctx.strokeStyle = 'rgba(76, 175, 80, 0.5)'; |
|
ctx.lineWidth = 2; |
|
ctx.beginPath(); |
|
ctx.moveTo(this.path[0].x, this.path[0].y); |
|
for (let i = 1; i < this.path.length; i++) { |
|
ctx.lineTo(this.path[i].x, this.path[i].y); |
|
} |
|
ctx.stroke(); |
|
} |
|
} |
|
} |
|
|
|
|
|
class Obstacle { |
|
constructor(x, y, size = 20) { |
|
this.x = x; |
|
this.y = y; |
|
this.size = size; |
|
} |
|
|
|
draw() { |
|
ctx.fillStyle = '#f44336'; |
|
ctx.beginPath(); |
|
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); |
|
ctx.fill(); |
|
|
|
|
|
ctx.strokeStyle = '#fff'; |
|
ctx.lineWidth = 2; |
|
ctx.beginPath(); |
|
ctx.moveTo(this.x - this.size/2, this.y - this.size/2); |
|
ctx.lineTo(this.x + this.size/2, this.y + this.size/2); |
|
ctx.moveTo(this.x + this.size/2, this.y - this.size/2); |
|
ctx.lineTo(this.x - this.size/2, this.y + this.size/2); |
|
ctx.stroke(); |
|
} |
|
} |
|
|
|
|
|
class Target { |
|
constructor(x, y) { |
|
this.x = x; |
|
this.y = y; |
|
this.size = 15; |
|
this.pulsePhase = Math.random() * Math.PI * 2; |
|
} |
|
|
|
update(deltaTime) { |
|
this.pulsePhase += deltaTime * 0.003; |
|
} |
|
|
|
draw() { |
|
const pulse = Math.sin(this.pulsePhase) * 0.3 + 1; |
|
const size = this.size * pulse; |
|
|
|
ctx.fillStyle = '#2196F3'; |
|
ctx.beginPath(); |
|
ctx.arc(this.x, this.y, size, 0, Math.PI * 2); |
|
ctx.fill(); |
|
|
|
|
|
ctx.strokeStyle = '#fff'; |
|
ctx.lineWidth = 2; |
|
ctx.beginPath(); |
|
ctx.arc(this.x, this.y, size * 0.7, 0, Math.PI * 2); |
|
ctx.stroke(); |
|
ctx.beginPath(); |
|
ctx.arc(this.x, this.y, size * 0.4, 0, Math.PI * 2); |
|
ctx.stroke(); |
|
} |
|
} |
|
|
|
|
|
const vehicle = new AutonomousVehicle(100, 100); |
|
const obstacles = []; |
|
const targets = []; |
|
|
|
|
|
const stats = { |
|
distanceTraveled: 0, |
|
targetsReached: 0, |
|
obstaclesAvoided: 0, |
|
decisionsPerSec: 0 |
|
}; |
|
|
|
|
|
function initializeEnvironment() { |
|
obstacles.length = 0; |
|
targets.length = 0; |
|
|
|
|
|
obstacles.push(new Obstacle(300, 200, 25)); |
|
obstacles.push(new Obstacle(500, 350, 30)); |
|
obstacles.push(new Obstacle(200, 400, 20)); |
|
obstacles.push(new Obstacle(600, 150, 35)); |
|
|
|
|
|
targets.push(new Target(400, 100)); |
|
targets.push(new Target(600, 400)); |
|
targets.push(new Target(150, 300)); |
|
} |
|
|
|
|
|
document.getElementById('startBtn').addEventListener('click', () => { |
|
isRunning = true; |
|
animate(); |
|
}); |
|
|
|
document.getElementById('pauseBtn').addEventListener('click', () => { |
|
isRunning = false; |
|
if (animationId) { |
|
cancelAnimationFrame(animationId); |
|
} |
|
}); |
|
|
|
document.getElementById('resetBtn').addEventListener('click', () => { |
|
isRunning = false; |
|
if (animationId) { |
|
cancelAnimationFrame(animationId); |
|
} |
|
|
|
|
|
vehicle.x = 100; |
|
vehicle.y = 100; |
|
vehicle.vx = 0; |
|
vehicle.vy = 0; |
|
vehicle.distanceTraveled = 0; |
|
vehicle.path = []; |
|
|
|
|
|
stats.targetsReached = 0; |
|
stats.obstaclesAvoided = 0; |
|
decisionCount = 0; |
|
|
|
|
|
initializeEnvironment(); |
|
|
|
|
|
updateStatsDisplay(); |
|
}); |
|
|
|
document.getElementById('speedSlider').addEventListener('input', (e) => { |
|
vehicle.speed = parseFloat(e.target.value); |
|
document.getElementById('speedValue').textContent = vehicle.speed.toFixed(1); |
|
}); |
|
|
|
document.getElementById('sensorRange').addEventListener('input', (e) => { |
|
vehicle.sensorRange = parseInt(e.target.value); |
|
document.getElementById('sensorValue').textContent = vehicle.sensorRange; |
|
}); |
|
|
|
document.getElementById('behaviorSelect').addEventListener('change', (e) => { |
|
vehicle.behavior = e.target.value; |
|
}); |
|
|
|
document.getElementById('addObstacleBtn').addEventListener('click', () => { |
|
const x = Math.random() * (canvas.width - 100) + 50; |
|
const y = Math.random() * (canvas.height - 100) + 50; |
|
const size = Math.random() * 20 + 15; |
|
obstacles.push(new Obstacle(x, y, size)); |
|
}); |
|
|
|
document.getElementById('addTargetBtn').addEventListener('click', () => { |
|
const x = Math.random() * (canvas.width - 100) + 50; |
|
const y = Math.random() * (canvas.height - 100) + 50; |
|
targets.push(new Target(x, y)); |
|
}); |
|
|
|
document.getElementById('clearBtn').addEventListener('click', () => { |
|
obstacles.length = 0; |
|
targets.length = 0; |
|
}); |
|
|
|
|
|
canvas.addEventListener('click', (e) => { |
|
const rect = canvas.getBoundingClientRect(); |
|
const x = e.clientX - rect.left; |
|
const y = e.clientY - rect.top; |
|
|
|
if (e.shiftKey) { |
|
obstacles.push(new Obstacle(x, y, 20)); |
|
} else { |
|
targets.push(new Target(x, y)); |
|
} |
|
}); |
|
|
|
|
|
function updateStatsDisplay() { |
|
document.getElementById('distanceTraveled').textContent = Math.round(vehicle.distanceTraveled) + 'm'; |
|
document.getElementById('targetsReached').textContent = stats.targetsReached; |
|
document.getElementById('obstaclesAvoided').textContent = stats.obstaclesAvoided; |
|
document.getElementById('decisionsPerSec').textContent = stats.decisionsPerSec; |
|
} |
|
|
|
|
|
function animate(currentTime = 0) { |
|
if (!isRunning) return; |
|
|
|
const deltaTime = currentTime - lastTime; |
|
lastTime = currentTime; |
|
|
|
|
|
if (currentTime - lastDecisionTime > 1000) { |
|
stats.decisionsPerSec = Math.round((decisionCount / (currentTime - lastDecisionTime)) * 1000); |
|
decisionCount = 0; |
|
lastDecisionTime = currentTime; |
|
} |
|
|
|
|
|
ctx.fillStyle = '#1a1a2e'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
for (let target of targets) { |
|
target.update(deltaTime); |
|
target.draw(); |
|
} |
|
|
|
|
|
for (let obstacle of obstacles) { |
|
obstacle.draw(); |
|
} |
|
|
|
|
|
vehicle.update(deltaTime); |
|
vehicle.draw(); |
|
|
|
|
|
updateStatsDisplay(); |
|
|
|
animationId = requestAnimationFrame(animate); |
|
} |
|
|
|
|
|
initializeEnvironment(); |
|
animate(); |
|
|
|
|
|
const instructions = document.createElement('div'); |
|
instructions.style.cssText = ` |
|
position: fixed; |
|
top: 20px; |
|
right: 20px; |
|
background: rgba(0, 0, 0, 0.8); |
|
color: white; |
|
padding: 15px; |
|
border-radius: 10px; |
|
font-size: 12px; |
|
max-width: 200px; |
|
z-index: 1000; |
|
`; |
|
instructions.innerHTML = ` |
|
<strong>Controls:</strong><br> |
|
• Click to add targets<br> |
|
• Shift+Click to add obstacles<br> |
|
• Use panel controls for settings<br> |
|
• Watch AI make real-time decisions! |
|
`; |
|
document.body.appendChild(instructions); |
|
|
|
|
|
setTimeout(() => { |
|
instructions.style.opacity = '0'; |
|
instructions.style.transition = 'opacity 1s'; |
|
setTimeout(() => instructions.remove(), 1000); |
|
}, 5000); |
|
</script> |
|
</body> |
|
</html> |