Spaces:
Running
Running
| <!-- Airplane Simulator By Pejman Ebrahimi --> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Simple RL Airplane Simulator</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #f0f8ff; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| canvas { | |
| background: linear-gradient(to bottom, #87ceeb, #e0f7ff); | |
| border: 1px solid #333; | |
| margin-bottom: 20px; | |
| } | |
| .controls { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 20px; | |
| max-width: 800px; | |
| margin-bottom: 20px; | |
| } | |
| .control-group { | |
| background-color: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | |
| min-width: 250px; | |
| } | |
| h1, | |
| h2 { | |
| color: #1e3a8a; | |
| } | |
| button { | |
| padding: 10px 15px; | |
| margin: 5px; | |
| background-color: #1e3a8a; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| button:hover { | |
| background-color: #162a61; | |
| } | |
| button:disabled { | |
| background-color: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .param-group { | |
| margin-bottom: 10px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 5px; | |
| font-weight: bold; | |
| } | |
| .footer { | |
| margin-top: 30px; | |
| text-align: center; | |
| color: #555; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Simple RL Airplane Simulator</h1> | |
| <canvas id="canvas" width="600" height="400"></canvas> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <h2>RL Parameters</h2> | |
| <div class="param-group"> | |
| <label for="learningRate" | |
| >Learning Rate: <span id="learningRateValue">0.1</span></label | |
| > | |
| <input | |
| type="range" | |
| id="learningRate" | |
| min="0.01" | |
| max="0.5" | |
| step="0.01" | |
| value="0.1" | |
| /> | |
| </div> | |
| <div class="param-group"> | |
| <label for="epsilon" | |
| >Exploration Rate: <span id="epsilonValue">0.3</span></label | |
| > | |
| <input | |
| type="range" | |
| id="epsilon" | |
| min="0.01" | |
| max="1" | |
| step="0.01" | |
| value="0.3" | |
| /> | |
| </div> | |
| <div class="param-group"> | |
| <label for="gamma" | |
| >Discount Factor: <span id="gammaValue">0.9</span></label | |
| > | |
| <input | |
| type="range" | |
| id="gamma" | |
| min="0.5" | |
| max="0.99" | |
| step="0.01" | |
| value="0.9" | |
| /> | |
| </div> | |
| <button id="startBtn">Start Learning</button> | |
| <button id="resetBtn">Reset Agent</button> | |
| <button id="toggleHumanBtn">Toggle Human Control</button> | |
| </div> | |
| <div class="control-group"> | |
| <h2>Statistics</h2> | |
| <p>Episode: <span id="episodeCounter">0</span></p> | |
| <p>Current Reward: <span id="currentReward">0</span></p> | |
| <p>Average Reward: <span id="avgReward">0</span></p> | |
| <p>Altitude: <span id="altitude">100</span> m</p> | |
| <p>Speed: <span id="speed">150</span> km/h</p> | |
| <p>Pitch: <span id="pitch">0</span>°</p> | |
| <p>Human Control: <span id="humanControlStatus">Off</span></p> | |
| </div> | |
| </div> | |
| <div class="control-group" style="width: 100%; max-width: 800px"> | |
| <h2>Learning Progress</h2> | |
| <div | |
| id="infoPanel" | |
| style=" | |
| height: 100px; | |
| overflow-y: auto; | |
| border: 1px solid #ccc; | |
| padding: 10px; | |
| background-color: #f9f9f9; | |
| " | |
| ></div> | |
| </div> | |
| <div class="footer"> | |
| <p>Flight Simulator Designed by Pejman</p> | |
| <p>© 2025 Pejman Ebrahimi - All Rights Reserved</p> | |
| </div> | |
| <script> | |
| // Get canvas and context | |
| const canvas = document.getElementById("canvas"); | |
| const ctx = canvas.getContext("2d"); | |
| // Game state | |
| let isLearning = false; | |
| let isHumanControl = false; | |
| let frameCount = 0; | |
| let episode = 0; | |
| let currentReward = 0; | |
| let rewardHistory = []; | |
| // Airplane state | |
| const airplane = { | |
| x: 300, | |
| y: 200, | |
| altitude: 100, | |
| speed: 150, | |
| pitch: 0, | |
| crashed: false, | |
| }; | |
| // RL parameters | |
| let learningRate = 0.1; | |
| let epsilon = 0.3; | |
| let gamma = 0.9; | |
| // Q-learning variables | |
| const qTable = {}; | |
| const actions = [ | |
| "nothing", | |
| "pitch_up", | |
| "pitch_down", | |
| "throttle_up", | |
| "throttle_down", | |
| ]; | |
| // Key states for human control | |
| const keys = { | |
| up: false, | |
| down: false, | |
| left: false, | |
| right: false, | |
| }; | |
| // UI elements | |
| const startBtn = document.getElementById("startBtn"); | |
| const resetBtn = document.getElementById("resetBtn"); | |
| const toggleHumanBtn = document.getElementById("toggleHumanBtn"); | |
| const infoPanel = document.getElementById("infoPanel"); | |
| // Initialize event listeners | |
| function init() { | |
| // Parameter sliders | |
| document | |
| .getElementById("learningRate") | |
| .addEventListener("input", function () { | |
| learningRate = parseFloat(this.value); | |
| document.getElementById("learningRateValue").textContent = | |
| learningRate.toFixed(2); | |
| }); | |
| document | |
| .getElementById("epsilon") | |
| .addEventListener("input", function () { | |
| epsilon = parseFloat(this.value); | |
| document.getElementById("epsilonValue").textContent = | |
| epsilon.toFixed(2); | |
| }); | |
| document.getElementById("gamma").addEventListener("input", function () { | |
| gamma = parseFloat(this.value); | |
| document.getElementById("gammaValue").textContent = gamma.toFixed(2); | |
| }); | |
| // Buttons | |
| startBtn.addEventListener("click", function () { | |
| console.log("Start button clicked"); | |
| if (!isLearning) { | |
| isLearning = true; | |
| isHumanControl = false; | |
| episode = 0; | |
| resetAirplane(); | |
| startBtn.textContent = "Learning..."; | |
| startBtn.disabled = true; | |
| document.getElementById("humanControlStatus").textContent = "Off"; | |
| logInfo("Started learning process"); | |
| } | |
| }); | |
| resetBtn.addEventListener("click", function () { | |
| isLearning = false; | |
| resetAirplane(); | |
| qTable = {}; | |
| episode = 0; | |
| rewardHistory = []; | |
| startBtn.textContent = "Start Learning"; | |
| startBtn.disabled = false; | |
| logInfo("Reset agent and Q-table"); | |
| updateStats(); | |
| }); | |
| toggleHumanBtn.addEventListener("click", function () { | |
| isHumanControl = !isHumanControl; | |
| isLearning = false; | |
| document.getElementById("humanControlStatus").textContent = | |
| isHumanControl ? "On" : "Off"; | |
| startBtn.disabled = isHumanControl; | |
| resetAirplane(); | |
| logInfo( | |
| isHumanControl | |
| ? "Switched to manual control" | |
| : "Switched to AI mode" | |
| ); | |
| }); | |
| // Keyboard controls | |
| window.addEventListener("keydown", function (e) { | |
| if (isHumanControl) { | |
| switch (e.key) { | |
| case "ArrowUp": | |
| keys.up = true; | |
| break; | |
| case "ArrowDown": | |
| keys.down = true; | |
| break; | |
| case "ArrowLeft": | |
| keys.left = true; | |
| break; | |
| case "ArrowRight": | |
| keys.right = true; | |
| break; | |
| } | |
| } | |
| }); | |
| window.addEventListener("keyup", function (e) { | |
| switch (e.key) { | |
| case "ArrowUp": | |
| keys.up = false; | |
| break; | |
| case "ArrowDown": | |
| keys.down = false; | |
| break; | |
| case "ArrowLeft": | |
| keys.left = false; | |
| break; | |
| case "ArrowRight": | |
| keys.right = false; | |
| break; | |
| } | |
| }); | |
| // Start game loop | |
| gameLoop(); | |
| } | |
| // Helper: Log info to panel | |
| function logInfo(message) { | |
| const entry = document.createElement("div"); | |
| entry.textContent = `Frame ${frameCount}: ${message}`; | |
| infoPanel.appendChild(entry); | |
| infoPanel.scrollTop = infoPanel.scrollHeight; | |
| console.log(message); // Also log to console for debugging | |
| } | |
| // Helper: Reset airplane | |
| function resetAirplane() { | |
| airplane.x = 300; | |
| airplane.y = 200; | |
| airplane.altitude = 100; | |
| airplane.speed = 150; | |
| airplane.pitch = 0; | |
| airplane.crashed = false; | |
| frameCount = 0; | |
| currentReward = 0; | |
| updateStats(); | |
| } | |
| // Helper: Update stats display | |
| function updateStats() { | |
| document.getElementById("episodeCounter").textContent = episode; | |
| document.getElementById("currentReward").textContent = | |
| currentReward.toFixed(1); | |
| document.getElementById("altitude").textContent = Math.round( | |
| airplane.altitude | |
| ); | |
| document.getElementById("speed").textContent = Math.round( | |
| airplane.speed | |
| ); | |
| document.getElementById("pitch").textContent = Math.round( | |
| airplane.pitch | |
| ); | |
| // Calculate average reward | |
| if (rewardHistory.length > 0) { | |
| const sum = rewardHistory.reduce((a, b) => a + b, 0); | |
| const avg = sum / rewardHistory.length; | |
| document.getElementById("avgReward").textContent = avg.toFixed(1); | |
| } else { | |
| document.getElementById("avgReward").textContent = "0"; | |
| } | |
| } | |
| // RL: Get state key for Q-table | |
| function getStateKey() { | |
| // Discretize the continuous state | |
| const altBucket = Math.floor(airplane.altitude / 50); | |
| const speedBucket = Math.floor(airplane.speed / 20); | |
| const pitchBucket = Math.floor(airplane.pitch / 5); | |
| return `${altBucket}_${speedBucket}_${pitchBucket}`; | |
| } | |
| // RL: Choose action using epsilon-greedy policy | |
| function chooseAction(stateKey) { | |
| // Ensure state exists in Q-table | |
| if (!qTable[stateKey]) { | |
| qTable[stateKey] = {}; | |
| actions.forEach((action) => { | |
| qTable[stateKey][action] = 0; | |
| }); | |
| } | |
| // Exploration: choose random action | |
| if (Math.random() < epsilon) { | |
| const randomAction = | |
| actions[Math.floor(Math.random() * actions.length)]; | |
| logInfo(`Exploring: Random action ${randomAction}`); | |
| return randomAction; | |
| } | |
| // Exploitation: choose best action | |
| let bestAction = actions[0]; | |
| let bestValue = qTable[stateKey][bestAction]; | |
| actions.forEach((action) => { | |
| if (qTable[stateKey][action] > bestValue) { | |
| bestValue = qTable[stateKey][action]; | |
| bestAction = action; | |
| } | |
| }); | |
| logInfo(`Exploiting: Best action ${bestAction}`); | |
| return bestAction; | |
| } | |
| // RL: Update Q-value | |
| function updateQValue(stateKey, action, reward, nextStateKey) { | |
| // Ensure states exist in Q-table | |
| if (!qTable[stateKey]) { | |
| qTable[stateKey] = {}; | |
| actions.forEach((a) => (qTable[stateKey][a] = 0)); | |
| } | |
| if (!qTable[nextStateKey]) { | |
| qTable[nextStateKey] = {}; | |
| actions.forEach((a) => (qTable[nextStateKey][a] = 0)); | |
| } | |
| // Find max Q-value for next state | |
| let maxNextQ = Math.max(...actions.map((a) => qTable[nextStateKey][a])); | |
| // Update rule: Q(s,a) = Q(s,a) + α * (r + γ * max(Q(s')) - Q(s,a)) | |
| const oldValue = qTable[stateKey][action]; | |
| const newValue = | |
| oldValue + learningRate * (reward + gamma * maxNextQ - oldValue); | |
| qTable[stateKey][action] = newValue; | |
| logInfo( | |
| `Updated Q(${stateKey}, ${action}): ${oldValue.toFixed( | |
| 2 | |
| )} → ${newValue.toFixed(2)}` | |
| ); | |
| } | |
| // Apply the chosen action | |
| function applyAction(action) { | |
| switch (action) { | |
| case "pitch_up": | |
| airplane.pitch += 2; | |
| break; | |
| case "pitch_down": | |
| airplane.pitch -= 2; | |
| break; | |
| case "throttle_up": | |
| airplane.speed += 10; | |
| break; | |
| case "throttle_down": | |
| airplane.speed -= 10; | |
| break; | |
| // 'nothing' case does nothing | |
| } | |
| // Clamp values to reasonable ranges | |
| airplane.pitch = Math.max(-30, Math.min(30, airplane.pitch)); | |
| airplane.speed = Math.max(50, Math.min(300, airplane.speed)); | |
| } | |
| // Apply human controls | |
| function applyHumanControl() { | |
| if (keys.up) { | |
| airplane.pitch += 1; | |
| } | |
| if (keys.down) { | |
| airplane.pitch -= 1; | |
| } | |
| if (keys.left) { | |
| airplane.speed -= 2; | |
| } | |
| if (keys.right) { | |
| airplane.speed += 2; | |
| } | |
| // Clamp values | |
| airplane.pitch = Math.max(-30, Math.min(30, airplane.pitch)); | |
| airplane.speed = Math.max(50, Math.min(300, airplane.speed)); | |
| } | |
| // Update physics | |
| function updatePhysics() { | |
| // Update altitude based on pitch and speed | |
| const pitchFactor = Math.sin((airplane.pitch * Math.PI) / 180); | |
| airplane.altitude += pitchFactor * airplane.speed * 0.05; | |
| // Apply gravity | |
| airplane.altitude -= 0.5; | |
| // Add some turbulence | |
| airplane.pitch += Math.random() - 0.5; | |
| // Gradual return to level flight (stabilization) | |
| airplane.pitch *= 0.98; | |
| // Check for crash | |
| if (airplane.altitude <= 0) { | |
| airplane.altitude = 0; | |
| airplane.crashed = true; | |
| logInfo("CRASHED!"); | |
| } | |
| updateStats(); | |
| } | |
| // Calculate reward | |
| function calculateReward() { | |
| let reward = 0; | |
| // Base reward for staying airborne | |
| reward += 1; | |
| // Reward for being at the target altitude (100-200m) | |
| if (airplane.altitude > 100 && airplane.altitude < 200) { | |
| reward += 2; | |
| } else if (airplane.altitude < 50 || airplane.altitude > 300) { | |
| reward -= 2; | |
| } | |
| // Reward for good speed | |
| if (airplane.speed > 100 && airplane.speed < 200) { | |
| reward += 1; | |
| } else if (airplane.speed < 80 || airplane.speed > 220) { | |
| reward -= 1; | |
| } | |
| // Penalty for extreme pitch | |
| if (Math.abs(airplane.pitch) > 20) { | |
| reward -= 2; | |
| } | |
| // Big penalty for crashing | |
| if (airplane.crashed) { | |
| reward -= 50; | |
| } | |
| return reward; | |
| } | |
| // Draw the scene | |
| function draw() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw sky | |
| const skyGradient = ctx.createLinearGradient(0, 0, 0, canvas.height); | |
| skyGradient.addColorStop(0, "#1e90ff"); | |
| skyGradient.addColorStop(1, "#87ceeb"); | |
| ctx.fillStyle = skyGradient; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw ground | |
| const groundY = canvas.height - airplane.altitude * 0.8; | |
| if (groundY < canvas.height) { | |
| ctx.fillStyle = "#8B4513"; | |
| ctx.fillRect(0, groundY, canvas.width, canvas.height); | |
| } | |
| // Draw airplane | |
| ctx.save(); | |
| ctx.translate(airplane.x, airplane.y); | |
| ctx.rotate((airplane.pitch * Math.PI) / 180); | |
| // Draw airplane body | |
| ctx.fillStyle = airplane.crashed ? "red" : "white"; | |
| ctx.beginPath(); | |
| ctx.moveTo(20, 0); // nose | |
| ctx.lineTo(-20, 10); // bottom rear | |
| ctx.lineTo(-10, 0); // tail | |
| ctx.lineTo(-20, -10); // top rear | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.strokeStyle = "#000"; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| // Draw wings | |
| ctx.fillStyle = "#ccc"; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, 0); | |
| ctx.lineTo(-5, 30); | |
| ctx.lineTo(5, 30); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(0, 0); | |
| ctx.lineTo(-5, -30); | |
| ctx.lineTo(5, -30); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| // Draw HUD | |
| ctx.font = "14px Arial"; | |
| ctx.fillStyle = "white"; | |
| ctx.fillText(`Altitude: ${Math.round(airplane.altitude)}m`, 20, 30); | |
| ctx.fillText(`Speed: ${Math.round(airplane.speed)} km/h`, 20, 50); | |
| ctx.fillText(`Pitch: ${Math.round(airplane.pitch)}°`, 20, 70); | |
| ctx.fillText( | |
| `Mode: ${isHumanControl ? "Human" : isLearning ? "Learning" : "AI"}`, | |
| 20, | |
| 90 | |
| ); | |
| // Draw altitude indicator | |
| const altIndicatorX = canvas.width - 50; | |
| const altIndicatorTop = 50; | |
| const altIndicatorHeight = 300; | |
| ctx.fillStyle = "rgba(0,0,0,0.5)"; | |
| ctx.fillRect( | |
| altIndicatorX - 15, | |
| altIndicatorTop, | |
| 30, | |
| altIndicatorHeight | |
| ); | |
| const markerPosition = | |
| altIndicatorTop + | |
| altIndicatorHeight - | |
| (airplane.altitude / 200) * altIndicatorHeight; | |
| ctx.fillStyle = "lime"; | |
| ctx.beginPath(); | |
| ctx.moveTo(altIndicatorX - 20, markerPosition); | |
| ctx.lineTo(altIndicatorX, markerPosition - 10); | |
| ctx.lineTo(altIndicatorX, markerPosition + 10); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| // Main game loop | |
| function gameLoop() { | |
| frameCount++; | |
| if (isHumanControl) { | |
| // Human control mode | |
| applyHumanControl(); | |
| updatePhysics(); | |
| } else if (isLearning) { | |
| // AI learning mode | |
| const currentState = getStateKey(); | |
| // Choose and apply action | |
| const action = chooseAction(currentState); | |
| applyAction(action); | |
| // Update physics | |
| updatePhysics(); | |
| // Calculate reward | |
| const reward = calculateReward(); | |
| currentReward += reward; | |
| // Get next state | |
| const nextState = getStateKey(); | |
| // Update Q-table | |
| updateQValue(currentState, action, reward, nextState); | |
| // Check if episode is complete | |
| if (airplane.crashed || frameCount > 1000) { | |
| // Log episode result | |
| logInfo( | |
| `Episode ${episode} finished with reward: ${currentReward.toFixed( | |
| 1 | |
| )}` | |
| ); | |
| rewardHistory.push(currentReward); | |
| // Move to next episode | |
| episode++; | |
| document.getElementById("episodeCounter").textContent = episode; | |
| // Reset for next episode | |
| resetAirplane(); | |
| // Reduce exploration rate over time (optional) | |
| if (epsilon > 0.05) { | |
| epsilon *= 0.99; | |
| document.getElementById("epsilonValue").textContent = | |
| epsilon.toFixed(2); | |
| document.getElementById("epsilon").value = epsilon; | |
| } | |
| } | |
| } else { | |
| // Idle mode - just apply physics with small drone movement | |
| if (frameCount % 60 === 0) { | |
| // Make small random adjustments | |
| airplane.pitch += (Math.random() - 0.5) * 2; | |
| } | |
| updatePhysics(); | |
| } | |
| // Draw everything | |
| draw(); | |
| // Continue game loop | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Start everything | |
| window.onload = init; | |
| </script> | |
| </body> | |
| </html> | |