Spaces:
Running
Running
Update index.html
Browse files- index.html +716 -18
index.html
CHANGED
@@ -1,19 +1,717 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
</html>
|
|
|
1 |
+
<!-- Airplane Simulator By Pejman Ebrahimi -->
|
2 |
+
<!DOCTYPE html>
|
3 |
+
<html lang="en">
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>Simple RL Airplane Simulator</title>
|
8 |
+
<style>
|
9 |
+
body {
|
10 |
+
font-family: Arial, sans-serif;
|
11 |
+
margin: 0;
|
12 |
+
padding: 20px;
|
13 |
+
background-color: #f0f8ff;
|
14 |
+
display: flex;
|
15 |
+
flex-direction: column;
|
16 |
+
align-items: center;
|
17 |
+
}
|
18 |
+
|
19 |
+
canvas {
|
20 |
+
background: linear-gradient(to bottom, #87ceeb, #e0f7ff);
|
21 |
+
border: 1px solid #333;
|
22 |
+
margin-bottom: 20px;
|
23 |
+
}
|
24 |
+
|
25 |
+
.controls {
|
26 |
+
display: flex;
|
27 |
+
flex-wrap: wrap;
|
28 |
+
gap: 20px;
|
29 |
+
max-width: 800px;
|
30 |
+
margin-bottom: 20px;
|
31 |
+
}
|
32 |
+
|
33 |
+
.control-group {
|
34 |
+
background-color: white;
|
35 |
+
padding: 15px;
|
36 |
+
border-radius: 8px;
|
37 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
38 |
+
min-width: 250px;
|
39 |
+
}
|
40 |
+
|
41 |
+
h1,
|
42 |
+
h2 {
|
43 |
+
color: #1e3a8a;
|
44 |
+
}
|
45 |
+
|
46 |
+
button {
|
47 |
+
padding: 10px 15px;
|
48 |
+
margin: 5px;
|
49 |
+
background-color: #1e3a8a;
|
50 |
+
color: white;
|
51 |
+
border: none;
|
52 |
+
border-radius: 4px;
|
53 |
+
cursor: pointer;
|
54 |
+
}
|
55 |
+
|
56 |
+
button:hover {
|
57 |
+
background-color: #162a61;
|
58 |
+
}
|
59 |
+
|
60 |
+
button:disabled {
|
61 |
+
background-color: #ccc;
|
62 |
+
cursor: not-allowed;
|
63 |
+
}
|
64 |
+
|
65 |
+
.param-group {
|
66 |
+
margin-bottom: 10px;
|
67 |
+
}
|
68 |
+
|
69 |
+
label {
|
70 |
+
display: block;
|
71 |
+
margin-bottom: 5px;
|
72 |
+
font-weight: bold;
|
73 |
+
}
|
74 |
+
|
75 |
+
.footer {
|
76 |
+
margin-top: 30px;
|
77 |
+
text-align: center;
|
78 |
+
color: #555;
|
79 |
+
}
|
80 |
+
</style>
|
81 |
+
</head>
|
82 |
+
<body>
|
83 |
+
<h1>Simple RL Airplane Simulator</h1>
|
84 |
+
|
85 |
+
<canvas id="canvas" width="600" height="400"></canvas>
|
86 |
+
|
87 |
+
<div class="controls">
|
88 |
+
<div class="control-group">
|
89 |
+
<h2>RL Parameters</h2>
|
90 |
+
|
91 |
+
<div class="param-group">
|
92 |
+
<label for="learningRate"
|
93 |
+
>Learning Rate: <span id="learningRateValue">0.1</span></label
|
94 |
+
>
|
95 |
+
<input
|
96 |
+
type="range"
|
97 |
+
id="learningRate"
|
98 |
+
min="0.01"
|
99 |
+
max="0.5"
|
100 |
+
step="0.01"
|
101 |
+
value="0.1"
|
102 |
+
/>
|
103 |
+
</div>
|
104 |
+
|
105 |
+
<div class="param-group">
|
106 |
+
<label for="epsilon"
|
107 |
+
>Exploration Rate: <span id="epsilonValue">0.3</span></label
|
108 |
+
>
|
109 |
+
<input
|
110 |
+
type="range"
|
111 |
+
id="epsilon"
|
112 |
+
min="0.01"
|
113 |
+
max="1"
|
114 |
+
step="0.01"
|
115 |
+
value="0.3"
|
116 |
+
/>
|
117 |
+
</div>
|
118 |
+
|
119 |
+
<div class="param-group">
|
120 |
+
<label for="gamma"
|
121 |
+
>Discount Factor: <span id="gammaValue">0.9</span></label
|
122 |
+
>
|
123 |
+
<input
|
124 |
+
type="range"
|
125 |
+
id="gamma"
|
126 |
+
min="0.5"
|
127 |
+
max="0.99"
|
128 |
+
step="0.01"
|
129 |
+
value="0.9"
|
130 |
+
/>
|
131 |
+
</div>
|
132 |
+
|
133 |
+
<button id="startBtn">Start Learning</button>
|
134 |
+
<button id="resetBtn">Reset Agent</button>
|
135 |
+
<button id="toggleHumanBtn">Toggle Human Control</button>
|
136 |
+
</div>
|
137 |
+
|
138 |
+
<div class="control-group">
|
139 |
+
<h2>Statistics</h2>
|
140 |
+
<p>Episode: <span id="episodeCounter">0</span></p>
|
141 |
+
<p>Current Reward: <span id="currentReward">0</span></p>
|
142 |
+
<p>Average Reward: <span id="avgReward">0</span></p>
|
143 |
+
<p>Altitude: <span id="altitude">100</span> m</p>
|
144 |
+
<p>Speed: <span id="speed">150</span> km/h</p>
|
145 |
+
<p>Pitch: <span id="pitch">0</span>°</p>
|
146 |
+
<p>Human Control: <span id="humanControlStatus">Off</span></p>
|
147 |
+
</div>
|
148 |
+
</div>
|
149 |
+
|
150 |
+
<div class="control-group" style="width: 100%; max-width: 800px">
|
151 |
+
<h2>Learning Progress</h2>
|
152 |
+
<div
|
153 |
+
id="infoPanel"
|
154 |
+
style="
|
155 |
+
height: 100px;
|
156 |
+
overflow-y: auto;
|
157 |
+
border: 1px solid #ccc;
|
158 |
+
padding: 10px;
|
159 |
+
background-color: #f9f9f9;
|
160 |
+
"
|
161 |
+
></div>
|
162 |
+
</div>
|
163 |
+
|
164 |
+
<div class="footer">
|
165 |
+
<p>Flight Simulator Designed by Pejman</p>
|
166 |
+
<p>© 2025 Pejman Ebrahimi - All Rights Reserved</p>
|
167 |
+
</div>
|
168 |
+
|
169 |
+
<script>
|
170 |
+
// Get canvas and context
|
171 |
+
const canvas = document.getElementById("canvas");
|
172 |
+
const ctx = canvas.getContext("2d");
|
173 |
+
|
174 |
+
// Game state
|
175 |
+
let isLearning = false;
|
176 |
+
let isHumanControl = false;
|
177 |
+
let frameCount = 0;
|
178 |
+
let episode = 0;
|
179 |
+
let currentReward = 0;
|
180 |
+
let rewardHistory = [];
|
181 |
+
|
182 |
+
// Airplane state
|
183 |
+
const airplane = {
|
184 |
+
x: 300,
|
185 |
+
y: 200,
|
186 |
+
altitude: 100,
|
187 |
+
speed: 150,
|
188 |
+
pitch: 0,
|
189 |
+
crashed: false,
|
190 |
+
};
|
191 |
+
|
192 |
+
// RL parameters
|
193 |
+
let learningRate = 0.1;
|
194 |
+
let epsilon = 0.3;
|
195 |
+
let gamma = 0.9;
|
196 |
+
|
197 |
+
// Q-learning variables
|
198 |
+
const qTable = {};
|
199 |
+
const actions = [
|
200 |
+
"nothing",
|
201 |
+
"pitch_up",
|
202 |
+
"pitch_down",
|
203 |
+
"throttle_up",
|
204 |
+
"throttle_down",
|
205 |
+
];
|
206 |
+
|
207 |
+
// Key states for human control
|
208 |
+
const keys = {
|
209 |
+
up: false,
|
210 |
+
down: false,
|
211 |
+
left: false,
|
212 |
+
right: false,
|
213 |
+
};
|
214 |
+
|
215 |
+
// UI elements
|
216 |
+
const startBtn = document.getElementById("startBtn");
|
217 |
+
const resetBtn = document.getElementById("resetBtn");
|
218 |
+
const toggleHumanBtn = document.getElementById("toggleHumanBtn");
|
219 |
+
const infoPanel = document.getElementById("infoPanel");
|
220 |
+
|
221 |
+
// Initialize event listeners
|
222 |
+
function init() {
|
223 |
+
// Parameter sliders
|
224 |
+
document
|
225 |
+
.getElementById("learningRate")
|
226 |
+
.addEventListener("input", function () {
|
227 |
+
learningRate = parseFloat(this.value);
|
228 |
+
document.getElementById("learningRateValue").textContent =
|
229 |
+
learningRate.toFixed(2);
|
230 |
+
});
|
231 |
+
|
232 |
+
document
|
233 |
+
.getElementById("epsilon")
|
234 |
+
.addEventListener("input", function () {
|
235 |
+
epsilon = parseFloat(this.value);
|
236 |
+
document.getElementById("epsilonValue").textContent =
|
237 |
+
epsilon.toFixed(2);
|
238 |
+
});
|
239 |
+
|
240 |
+
document.getElementById("gamma").addEventListener("input", function () {
|
241 |
+
gamma = parseFloat(this.value);
|
242 |
+
document.getElementById("gammaValue").textContent = gamma.toFixed(2);
|
243 |
+
});
|
244 |
+
|
245 |
+
// Buttons
|
246 |
+
startBtn.addEventListener("click", function () {
|
247 |
+
console.log("Start button clicked");
|
248 |
+
if (!isLearning) {
|
249 |
+
isLearning = true;
|
250 |
+
isHumanControl = false;
|
251 |
+
episode = 0;
|
252 |
+
resetAirplane();
|
253 |
+
startBtn.textContent = "Learning...";
|
254 |
+
startBtn.disabled = true;
|
255 |
+
document.getElementById("humanControlStatus").textContent = "Off";
|
256 |
+
logInfo("Started learning process");
|
257 |
+
}
|
258 |
+
});
|
259 |
+
|
260 |
+
resetBtn.addEventListener("click", function () {
|
261 |
+
isLearning = false;
|
262 |
+
resetAirplane();
|
263 |
+
qTable = {};
|
264 |
+
episode = 0;
|
265 |
+
rewardHistory = [];
|
266 |
+
startBtn.textContent = "Start Learning";
|
267 |
+
startBtn.disabled = false;
|
268 |
+
logInfo("Reset agent and Q-table");
|
269 |
+
updateStats();
|
270 |
+
});
|
271 |
+
|
272 |
+
toggleHumanBtn.addEventListener("click", function () {
|
273 |
+
isHumanControl = !isHumanControl;
|
274 |
+
isLearning = false;
|
275 |
+
document.getElementById("humanControlStatus").textContent =
|
276 |
+
isHumanControl ? "On" : "Off";
|
277 |
+
startBtn.disabled = isHumanControl;
|
278 |
+
resetAirplane();
|
279 |
+
logInfo(
|
280 |
+
isHumanControl
|
281 |
+
? "Switched to manual control"
|
282 |
+
: "Switched to AI mode"
|
283 |
+
);
|
284 |
+
});
|
285 |
+
|
286 |
+
// Keyboard controls
|
287 |
+
window.addEventListener("keydown", function (e) {
|
288 |
+
if (isHumanControl) {
|
289 |
+
switch (e.key) {
|
290 |
+
case "ArrowUp":
|
291 |
+
keys.up = true;
|
292 |
+
break;
|
293 |
+
case "ArrowDown":
|
294 |
+
keys.down = true;
|
295 |
+
break;
|
296 |
+
case "ArrowLeft":
|
297 |
+
keys.left = true;
|
298 |
+
break;
|
299 |
+
case "ArrowRight":
|
300 |
+
keys.right = true;
|
301 |
+
break;
|
302 |
+
}
|
303 |
+
}
|
304 |
+
});
|
305 |
+
|
306 |
+
window.addEventListener("keyup", function (e) {
|
307 |
+
switch (e.key) {
|
308 |
+
case "ArrowUp":
|
309 |
+
keys.up = false;
|
310 |
+
break;
|
311 |
+
case "ArrowDown":
|
312 |
+
keys.down = false;
|
313 |
+
break;
|
314 |
+
case "ArrowLeft":
|
315 |
+
keys.left = false;
|
316 |
+
break;
|
317 |
+
case "ArrowRight":
|
318 |
+
keys.right = false;
|
319 |
+
break;
|
320 |
+
}
|
321 |
+
});
|
322 |
+
|
323 |
+
// Start game loop
|
324 |
+
gameLoop();
|
325 |
+
}
|
326 |
+
|
327 |
+
// Helper: Log info to panel
|
328 |
+
function logInfo(message) {
|
329 |
+
const entry = document.createElement("div");
|
330 |
+
entry.textContent = `Frame ${frameCount}: ${message}`;
|
331 |
+
infoPanel.appendChild(entry);
|
332 |
+
infoPanel.scrollTop = infoPanel.scrollHeight;
|
333 |
+
console.log(message); // Also log to console for debugging
|
334 |
+
}
|
335 |
+
|
336 |
+
// Helper: Reset airplane
|
337 |
+
function resetAirplane() {
|
338 |
+
airplane.x = 300;
|
339 |
+
airplane.y = 200;
|
340 |
+
airplane.altitude = 100;
|
341 |
+
airplane.speed = 150;
|
342 |
+
airplane.pitch = 0;
|
343 |
+
airplane.crashed = false;
|
344 |
+
frameCount = 0;
|
345 |
+
currentReward = 0;
|
346 |
+
updateStats();
|
347 |
+
}
|
348 |
+
|
349 |
+
// Helper: Update stats display
|
350 |
+
function updateStats() {
|
351 |
+
document.getElementById("episodeCounter").textContent = episode;
|
352 |
+
document.getElementById("currentReward").textContent =
|
353 |
+
currentReward.toFixed(1);
|
354 |
+
document.getElementById("altitude").textContent = Math.round(
|
355 |
+
airplane.altitude
|
356 |
+
);
|
357 |
+
document.getElementById("speed").textContent = Math.round(
|
358 |
+
airplane.speed
|
359 |
+
);
|
360 |
+
document.getElementById("pitch").textContent = Math.round(
|
361 |
+
airplane.pitch
|
362 |
+
);
|
363 |
+
|
364 |
+
// Calculate average reward
|
365 |
+
if (rewardHistory.length > 0) {
|
366 |
+
const sum = rewardHistory.reduce((a, b) => a + b, 0);
|
367 |
+
const avg = sum / rewardHistory.length;
|
368 |
+
document.getElementById("avgReward").textContent = avg.toFixed(1);
|
369 |
+
} else {
|
370 |
+
document.getElementById("avgReward").textContent = "0";
|
371 |
+
}
|
372 |
+
}
|
373 |
+
|
374 |
+
// RL: Get state key for Q-table
|
375 |
+
function getStateKey() {
|
376 |
+
// Discretize the continuous state
|
377 |
+
const altBucket = Math.floor(airplane.altitude / 50);
|
378 |
+
const speedBucket = Math.floor(airplane.speed / 20);
|
379 |
+
const pitchBucket = Math.floor(airplane.pitch / 5);
|
380 |
+
|
381 |
+
return `${altBucket}_${speedBucket}_${pitchBucket}`;
|
382 |
+
}
|
383 |
+
|
384 |
+
// RL: Choose action using epsilon-greedy policy
|
385 |
+
function chooseAction(stateKey) {
|
386 |
+
// Ensure state exists in Q-table
|
387 |
+
if (!qTable[stateKey]) {
|
388 |
+
qTable[stateKey] = {};
|
389 |
+
actions.forEach((action) => {
|
390 |
+
qTable[stateKey][action] = 0;
|
391 |
+
});
|
392 |
+
}
|
393 |
+
|
394 |
+
// Exploration: choose random action
|
395 |
+
if (Math.random() < epsilon) {
|
396 |
+
const randomAction =
|
397 |
+
actions[Math.floor(Math.random() * actions.length)];
|
398 |
+
logInfo(`Exploring: Random action ${randomAction}`);
|
399 |
+
return randomAction;
|
400 |
+
}
|
401 |
+
|
402 |
+
// Exploitation: choose best action
|
403 |
+
let bestAction = actions[0];
|
404 |
+
let bestValue = qTable[stateKey][bestAction];
|
405 |
+
|
406 |
+
actions.forEach((action) => {
|
407 |
+
if (qTable[stateKey][action] > bestValue) {
|
408 |
+
bestValue = qTable[stateKey][action];
|
409 |
+
bestAction = action;
|
410 |
+
}
|
411 |
+
});
|
412 |
+
|
413 |
+
logInfo(`Exploiting: Best action ${bestAction}`);
|
414 |
+
return bestAction;
|
415 |
+
}
|
416 |
+
|
417 |
+
// RL: Update Q-value
|
418 |
+
function updateQValue(stateKey, action, reward, nextStateKey) {
|
419 |
+
// Ensure states exist in Q-table
|
420 |
+
if (!qTable[stateKey]) {
|
421 |
+
qTable[stateKey] = {};
|
422 |
+
actions.forEach((a) => (qTable[stateKey][a] = 0));
|
423 |
+
}
|
424 |
+
|
425 |
+
if (!qTable[nextStateKey]) {
|
426 |
+
qTable[nextStateKey] = {};
|
427 |
+
actions.forEach((a) => (qTable[nextStateKey][a] = 0));
|
428 |
+
}
|
429 |
+
|
430 |
+
// Find max Q-value for next state
|
431 |
+
let maxNextQ = Math.max(...actions.map((a) => qTable[nextStateKey][a]));
|
432 |
+
|
433 |
+
// Update rule: Q(s,a) = Q(s,a) + α * (r + γ * max(Q(s')) - Q(s,a))
|
434 |
+
const oldValue = qTable[stateKey][action];
|
435 |
+
const newValue =
|
436 |
+
oldValue + learningRate * (reward + gamma * maxNextQ - oldValue);
|
437 |
+
qTable[stateKey][action] = newValue;
|
438 |
+
|
439 |
+
logInfo(
|
440 |
+
`Updated Q(${stateKey}, ${action}): ${oldValue.toFixed(
|
441 |
+
2
|
442 |
+
)} → ${newValue.toFixed(2)}`
|
443 |
+
);
|
444 |
+
}
|
445 |
+
|
446 |
+
// Apply the chosen action
|
447 |
+
function applyAction(action) {
|
448 |
+
switch (action) {
|
449 |
+
case "pitch_up":
|
450 |
+
airplane.pitch += 2;
|
451 |
+
break;
|
452 |
+
case "pitch_down":
|
453 |
+
airplane.pitch -= 2;
|
454 |
+
break;
|
455 |
+
case "throttle_up":
|
456 |
+
airplane.speed += 10;
|
457 |
+
break;
|
458 |
+
case "throttle_down":
|
459 |
+
airplane.speed -= 10;
|
460 |
+
break;
|
461 |
+
// 'nothing' case does nothing
|
462 |
+
}
|
463 |
+
|
464 |
+
// Clamp values to reasonable ranges
|
465 |
+
airplane.pitch = Math.max(-30, Math.min(30, airplane.pitch));
|
466 |
+
airplane.speed = Math.max(50, Math.min(300, airplane.speed));
|
467 |
+
}
|
468 |
+
|
469 |
+
// Apply human controls
|
470 |
+
function applyHumanControl() {
|
471 |
+
if (keys.up) {
|
472 |
+
airplane.pitch += 1;
|
473 |
+
}
|
474 |
+
if (keys.down) {
|
475 |
+
airplane.pitch -= 1;
|
476 |
+
}
|
477 |
+
if (keys.left) {
|
478 |
+
airplane.speed -= 2;
|
479 |
+
}
|
480 |
+
if (keys.right) {
|
481 |
+
airplane.speed += 2;
|
482 |
+
}
|
483 |
+
|
484 |
+
// Clamp values
|
485 |
+
airplane.pitch = Math.max(-30, Math.min(30, airplane.pitch));
|
486 |
+
airplane.speed = Math.max(50, Math.min(300, airplane.speed));
|
487 |
+
}
|
488 |
+
|
489 |
+
// Update physics
|
490 |
+
function updatePhysics() {
|
491 |
+
// Update altitude based on pitch and speed
|
492 |
+
const pitchFactor = Math.sin((airplane.pitch * Math.PI) / 180);
|
493 |
+
airplane.altitude += pitchFactor * airplane.speed * 0.05;
|
494 |
+
|
495 |
+
// Apply gravity
|
496 |
+
airplane.altitude -= 0.5;
|
497 |
+
|
498 |
+
// Add some turbulence
|
499 |
+
airplane.pitch += Math.random() - 0.5;
|
500 |
+
|
501 |
+
// Gradual return to level flight (stabilization)
|
502 |
+
airplane.pitch *= 0.98;
|
503 |
+
|
504 |
+
// Check for crash
|
505 |
+
if (airplane.altitude <= 0) {
|
506 |
+
airplane.altitude = 0;
|
507 |
+
airplane.crashed = true;
|
508 |
+
logInfo("CRASHED!");
|
509 |
+
}
|
510 |
+
|
511 |
+
updateStats();
|
512 |
+
}
|
513 |
+
|
514 |
+
// Calculate reward
|
515 |
+
function calculateReward() {
|
516 |
+
let reward = 0;
|
517 |
+
|
518 |
+
// Base reward for staying airborne
|
519 |
+
reward += 1;
|
520 |
+
|
521 |
+
// Reward for being at the target altitude (100-200m)
|
522 |
+
if (airplane.altitude > 100 && airplane.altitude < 200) {
|
523 |
+
reward += 2;
|
524 |
+
} else if (airplane.altitude < 50 || airplane.altitude > 300) {
|
525 |
+
reward -= 2;
|
526 |
+
}
|
527 |
+
|
528 |
+
// Reward for good speed
|
529 |
+
if (airplane.speed > 100 && airplane.speed < 200) {
|
530 |
+
reward += 1;
|
531 |
+
} else if (airplane.speed < 80 || airplane.speed > 220) {
|
532 |
+
reward -= 1;
|
533 |
+
}
|
534 |
+
|
535 |
+
// Penalty for extreme pitch
|
536 |
+
if (Math.abs(airplane.pitch) > 20) {
|
537 |
+
reward -= 2;
|
538 |
+
}
|
539 |
+
|
540 |
+
// Big penalty for crashing
|
541 |
+
if (airplane.crashed) {
|
542 |
+
reward -= 50;
|
543 |
+
}
|
544 |
+
|
545 |
+
return reward;
|
546 |
+
}
|
547 |
+
|
548 |
+
// Draw the scene
|
549 |
+
function draw() {
|
550 |
+
// Clear canvas
|
551 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
552 |
+
|
553 |
+
// Draw sky
|
554 |
+
const skyGradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
555 |
+
skyGradient.addColorStop(0, "#1e90ff");
|
556 |
+
skyGradient.addColorStop(1, "#87ceeb");
|
557 |
+
ctx.fillStyle = skyGradient;
|
558 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
559 |
+
|
560 |
+
// Draw ground
|
561 |
+
const groundY = canvas.height - airplane.altitude * 0.8;
|
562 |
+
if (groundY < canvas.height) {
|
563 |
+
ctx.fillStyle = "#8B4513";
|
564 |
+
ctx.fillRect(0, groundY, canvas.width, canvas.height);
|
565 |
+
}
|
566 |
+
|
567 |
+
// Draw airplane
|
568 |
+
ctx.save();
|
569 |
+
ctx.translate(airplane.x, airplane.y);
|
570 |
+
ctx.rotate((airplane.pitch * Math.PI) / 180);
|
571 |
+
|
572 |
+
// Draw airplane body
|
573 |
+
ctx.fillStyle = airplane.crashed ? "red" : "white";
|
574 |
+
ctx.beginPath();
|
575 |
+
ctx.moveTo(20, 0); // nose
|
576 |
+
ctx.lineTo(-20, 10); // bottom rear
|
577 |
+
ctx.lineTo(-10, 0); // tail
|
578 |
+
ctx.lineTo(-20, -10); // top rear
|
579 |
+
ctx.closePath();
|
580 |
+
ctx.fill();
|
581 |
+
ctx.strokeStyle = "#000";
|
582 |
+
ctx.lineWidth = 2;
|
583 |
+
ctx.stroke();
|
584 |
+
|
585 |
+
// Draw wings
|
586 |
+
ctx.fillStyle = "#ccc";
|
587 |
+
ctx.beginPath();
|
588 |
+
ctx.moveTo(0, 0);
|
589 |
+
ctx.lineTo(-5, 30);
|
590 |
+
ctx.lineTo(5, 30);
|
591 |
+
ctx.closePath();
|
592 |
+
ctx.fill();
|
593 |
+
ctx.stroke();
|
594 |
+
|
595 |
+
ctx.beginPath();
|
596 |
+
ctx.moveTo(0, 0);
|
597 |
+
ctx.lineTo(-5, -30);
|
598 |
+
ctx.lineTo(5, -30);
|
599 |
+
ctx.closePath();
|
600 |
+
ctx.fill();
|
601 |
+
ctx.stroke();
|
602 |
+
|
603 |
+
ctx.restore();
|
604 |
+
|
605 |
+
// Draw HUD
|
606 |
+
ctx.font = "14px Arial";
|
607 |
+
ctx.fillStyle = "white";
|
608 |
+
ctx.fillText(`Altitude: ${Math.round(airplane.altitude)}m`, 20, 30);
|
609 |
+
ctx.fillText(`Speed: ${Math.round(airplane.speed)} km/h`, 20, 50);
|
610 |
+
ctx.fillText(`Pitch: ${Math.round(airplane.pitch)}°`, 20, 70);
|
611 |
+
ctx.fillText(
|
612 |
+
`Mode: ${isHumanControl ? "Human" : isLearning ? "Learning" : "AI"}`,
|
613 |
+
20,
|
614 |
+
90
|
615 |
+
);
|
616 |
+
|
617 |
+
// Draw altitude indicator
|
618 |
+
const altIndicatorX = canvas.width - 50;
|
619 |
+
const altIndicatorTop = 50;
|
620 |
+
const altIndicatorHeight = 300;
|
621 |
+
|
622 |
+
ctx.fillStyle = "rgba(0,0,0,0.5)";
|
623 |
+
ctx.fillRect(
|
624 |
+
altIndicatorX - 15,
|
625 |
+
altIndicatorTop,
|
626 |
+
30,
|
627 |
+
altIndicatorHeight
|
628 |
+
);
|
629 |
+
|
630 |
+
const markerPosition =
|
631 |
+
altIndicatorTop +
|
632 |
+
altIndicatorHeight -
|
633 |
+
(airplane.altitude / 200) * altIndicatorHeight;
|
634 |
+
ctx.fillStyle = "lime";
|
635 |
+
ctx.beginPath();
|
636 |
+
ctx.moveTo(altIndicatorX - 20, markerPosition);
|
637 |
+
ctx.lineTo(altIndicatorX, markerPosition - 10);
|
638 |
+
ctx.lineTo(altIndicatorX, markerPosition + 10);
|
639 |
+
ctx.closePath();
|
640 |
+
ctx.fill();
|
641 |
+
}
|
642 |
+
|
643 |
+
// Main game loop
|
644 |
+
function gameLoop() {
|
645 |
+
frameCount++;
|
646 |
+
|
647 |
+
if (isHumanControl) {
|
648 |
+
// Human control mode
|
649 |
+
applyHumanControl();
|
650 |
+
updatePhysics();
|
651 |
+
} else if (isLearning) {
|
652 |
+
// AI learning mode
|
653 |
+
const currentState = getStateKey();
|
654 |
+
|
655 |
+
// Choose and apply action
|
656 |
+
const action = chooseAction(currentState);
|
657 |
+
applyAction(action);
|
658 |
+
|
659 |
+
// Update physics
|
660 |
+
updatePhysics();
|
661 |
+
|
662 |
+
// Calculate reward
|
663 |
+
const reward = calculateReward();
|
664 |
+
currentReward += reward;
|
665 |
+
|
666 |
+
// Get next state
|
667 |
+
const nextState = getStateKey();
|
668 |
+
|
669 |
+
// Update Q-table
|
670 |
+
updateQValue(currentState, action, reward, nextState);
|
671 |
+
|
672 |
+
// Check if episode is complete
|
673 |
+
if (airplane.crashed || frameCount > 1000) {
|
674 |
+
// Log episode result
|
675 |
+
logInfo(
|
676 |
+
`Episode ${episode} finished with reward: ${currentReward.toFixed(
|
677 |
+
1
|
678 |
+
)}`
|
679 |
+
);
|
680 |
+
rewardHistory.push(currentReward);
|
681 |
+
|
682 |
+
// Move to next episode
|
683 |
+
episode++;
|
684 |
+
document.getElementById("episodeCounter").textContent = episode;
|
685 |
+
|
686 |
+
// Reset for next episode
|
687 |
+
resetAirplane();
|
688 |
+
|
689 |
+
// Reduce exploration rate over time (optional)
|
690 |
+
if (epsilon > 0.05) {
|
691 |
+
epsilon *= 0.99;
|
692 |
+
document.getElementById("epsilonValue").textContent =
|
693 |
+
epsilon.toFixed(2);
|
694 |
+
document.getElementById("epsilon").value = epsilon;
|
695 |
+
}
|
696 |
+
}
|
697 |
+
} else {
|
698 |
+
// Idle mode - just apply physics with small drone movement
|
699 |
+
if (frameCount % 60 === 0) {
|
700 |
+
// Make small random adjustments
|
701 |
+
airplane.pitch += (Math.random() - 0.5) * 2;
|
702 |
+
}
|
703 |
+
updatePhysics();
|
704 |
+
}
|
705 |
+
|
706 |
+
// Draw everything
|
707 |
+
draw();
|
708 |
+
|
709 |
+
// Continue game loop
|
710 |
+
requestAnimationFrame(gameLoop);
|
711 |
+
}
|
712 |
+
|
713 |
+
// Start everything
|
714 |
+
window.onload = init;
|
715 |
+
</script>
|
716 |
+
</body>
|
717 |
</html>
|