mnm3 commited on
Commit
1a304a4
·
verified ·
1 Parent(s): 3211793

Upload 2 files

Browse files
Files changed (2) hide show
  1. ai.py +1052 -0
  2. hcr_imitation_model.pth +3 -0
ai.py ADDED
@@ -0,0 +1,1052 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, sys, shutil, time, math, struct, subprocess, socket, warnings, threading, json, logging
2
+ from pathlib import Path
3
+ from collections import deque
4
+ import numpy as np
5
+ import cv2, mss, psutil, pyautogui
6
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
7
+ import ctypes
8
+
9
+ try:
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.optim as optim
13
+ from torch.utils.data import TensorDataset, DataLoader
14
+ from tqdm.auto import tqdm
15
+ import optuna
16
+ from sklearn.model_selection import train_test_split
17
+ except ImportError:
18
+ print("\033[91mKluczowe biblioteki (torch, tqdm, optuna, scikit-learn) nie są zainstalowane.\033[0m")
19
+ print("\033[93mUżyj: pip install torch tqdm optuna scikit-learn keyboard\033[0m")
20
+ torch = nn = optim = TensorDataset = DataLoader = tqdm = optuna = train_test_split = None
21
+
22
+ try:
23
+ import keyboard
24
+ except ImportError:
25
+ print("\033[91mBiblioteka 'keyboard' nie jest zainstalowana. Opcja nagrywania będzie niedostępna.\033[0m")
26
+ print("\033[93mUżyj: pip install keyboard\033[0m")
27
+ keyboard = None
28
+
29
+ from http.server import HTTPServer, BaseHTTPRequestHandler
30
+
31
+ class Colors:
32
+ RESET, RED, GREEN, YELLOW, BLUE, CYAN, MAGENTA, BOLD = (
33
+ "\033[0m","\033[91m","\033[92m","\033[93m","\033[94m","\033[96m","\033[95m","\033[1m"
34
+ )
35
+
36
+ if os.name == 'nt':
37
+ class _CursorInfo(ctypes.Structure): _fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)]
38
+ def hide_cursor():
39
+ ci = _CursorInfo(); handle = ctypes.windll.kernel32.GetStdHandle(-11)
40
+ ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)); ci.visible = False
41
+ ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
42
+ def show_cursor():
43
+ handle = ctypes.windll.kernel32.GetStdHandle(-11); ci = _CursorInfo()
44
+ ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)); ci.visible = True
45
+ ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
46
+ def reset_cursor_position():
47
+ handle = ctypes.windll.kernel32.GetStdHandle(-11); ctypes.windll.kernel32.SetConsoleCursorPosition(handle, 0)
48
+ else:
49
+ def hide_cursor(): sys.stdout.write("\033[?25l"); sys.stdout.flush()
50
+ def show_cursor(): sys.stdout.write("\033[?25h"); sys.stdout.flush()
51
+ def reset_cursor_position(): sys.stdout.write("\x1b[H")
52
+
53
+ TYPE_ROPE = 0.0
54
+ TYPE_GROUND = 1.0
55
+
56
+ SCRIPT_DIR = Path(__file__).parent.resolve()
57
+ MODEL_SAVE_PATH = SCRIPT_DIR / "hcr_imitation_model.pth"
58
+ DEMONSTRATIONS_PATH = SCRIPT_DIR / "hcr_demonstrations.npz"
59
+ LOG_FILE_PATH = SCRIPT_DIR / "hcr_ai_log.txt"
60
+ OPTUNA_DB_PATH = SCRIPT_DIR / "hcr_imitation_optuna.db"
61
+
62
+ OPTUNA_TRIALS = 20
63
+ OPTUNA_EPOCHS_PER_TRIAL = 40
64
+ FINAL_MODEL_EPOCHS = 80
65
+
66
+ MONITOR_INDEX = 1
67
+ CAPTURE_RESOLUTION_WIDTH, CAPTURE_RESOLUTION_HEIGHT = 1280, 720
68
+ PROCESS_NAME = "HillClimbRacing.exe"
69
+ AUMID = "FINGERSOFT.HILLCLIMBRACING_xt3psb39rghm0!App"
70
+ VELOCITY_SMOOTHING_FACTOR = 0.1
71
+
72
+ DISTANCE_POINTER_CONFIG = {"base_offset": 0x28CA2C, "offsets": [0x130, 0x10C, 0x19C, 0xA8, 0xA8, 0x184, 0x164]}
73
+ MAP_POINTER_CONFIG = {"base_offset": 0x28CAB4, "offsets": []}
74
+ FUEL_POINTER_CONFIG = {"base_offset": 0x28CA2C, "offsets": [0x2A8]}
75
+
76
+ MAP_IDS_TO_INDEX = {1:0, 3:1, 4:2, 5:3, 7:4, 9:5, 11:6, 14:7, 16:8, 17:9, 18:10, 20:11, 25:12}
77
+ NUM_MAP_FEATURES = 13
78
+ MAP_ID_PROGRESSION = [1, 3, 4, 5, 7, 9, 11, 14, 16, 17, 18, 20, 25]
79
+
80
+ MAP_SELECT_BUTTON = (444, 513)
81
+ START_RACE_BUTTON = (1050, 511)
82
+ LIFE_CHECK_PIXEL_X, LIFE_CHECK_PIXEL_Y = 32, 31
83
+ LIFE_CHECK_EXACT_RGB = (200, 0, 8)
84
+
85
+ NUMBER_OF_RAYS, NUM_VERTICAL_SCANS = 37, 15
86
+ MAX_CAR_RAY_DISTANCE, MAX_VERTICAL_RAY_DISTANCE = 700, 700
87
+ NUM_ANGLE_FEATURES, NUM_VELOCITY_FEATURES = 2, 1
88
+ LOWER_TARGET_GROUND, UPPER_TARGET_GROUND = np.array([142,250,250]), np.array([142,255,255])
89
+ LOWER_ROPE, UPPER_ROPE = np.array([ 16, 94,188]), np.array([ 16, 94,188])
90
+ LOWER_PINK, UPPER_PINK = np.array([147,255,255]), np.array([147,255,255])
91
+ LOWER_YELLOW, UPPER_YELLOW = np.array([ 30,255,255]), np.array([ 30,255,255])
92
+
93
+ INTERRUPT_REQUESTED = threading.Event()
94
+ OBS_SIZE = (NUM_MAP_FEATURES + (NUMBER_OF_RAYS * 2) + (NUM_VERTICAL_SCANS * 2) + NUM_ANGLE_FEATURES + NUM_VELOCITY_FEATURES)
95
+
96
+ STATE_LOCK = threading.Lock()
97
+ DASH_STATE = {}
98
+
99
+ def publish_to_dashboard(obs_used: np.ndarray, action:int, probs:list, reward:float, info:dict, step:int):
100
+ global DASH_STATE
101
+ if obs_used is None or obs_used.size == 0: return
102
+ try:
103
+ i = 0
104
+ mp = obs_used[i:i+NUM_MAP_FEATURES].tolist(); i += NUM_MAP_FEATURES
105
+ rays_flat = obs_used[i:i+(NUMBER_OF_RAYS*2)].tolist(); i += (NUMBER_OF_RAYS*2)
106
+ vscan_flat = obs_used[i:i+(NUM_VERTICAL_SCANS*2)].tolist(); i += (NUM_VERTICAL_SCANS*2)
107
+ angle = obs_used[i:i+NUM_ANGLE_FEATURES].tolist(); i += NUM_ANGLE_FEATURES
108
+ vel = obs_used[i:i+NUM_VELOCITY_FEATURES].tolist()
109
+
110
+ with STATE_LOCK:
111
+ DASH_STATE = {
112
+ "step": int(step), "action": int(action), "action_probs": [float(p) for p in probs],
113
+ "reward": float(reward), "distance": int(info.get("distance", 0)),
114
+ "obs_parts": {
115
+ "map": mp,
116
+ "rays": [rays_flat[k:k+2] for k in range(0, len(rays_flat), 2)],
117
+ "vscan": [vscan_flat[k:k+2] for k in range(0, len(vscan_flat), 2)],
118
+ "angle": angle, "velocity": vel, "action_history_idx": []
119
+ },
120
+ "flat": obs_used.tolist()
121
+ }
122
+ except Exception: pass
123
+
124
+ _HTML = r"""<!doctype html><html lang="pl"><meta charset="utf-8">
125
+ <title>HCR PPO — Obserwacje i Akcje</title>
126
+ <meta name="viewport" content="width=device-width,initial-scale=1">
127
+ <style>
128
+ :root{--bg:#0b0f14;--card:#0f1624;--muted:#8aa3c2;--txt:#e5f0ff;--ring:#3b82f6;--green:#22c55e;--orange:#f59e0b;--gray:#64748b;--brown:#a16207;--miss:#475569}
129
+ *{box-sizing:border-box;font-family:ui-sans-serif,system-ui,Segoe UI,Roboto}
130
+ body{margin:0;background:linear-gradient(180deg,var(--bg),#0a1524 60%,#09182b);color:var(--txt);font-size:14px}
131
+ .wrap{max-width:1200px;margin:24px auto;padding:0 18px}
132
+ h1{font-size:26px;margin:0 0 6px;letter-spacing:-.025em}.sub{color:var(--muted);margin:0 0 18px;font-size:14px}
133
+ .grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(400px,1fr))}
134
+ .card{background:linear-gradient(180deg,rgba(255,255,255,.02),rgba(255,255,255,.01));border:1px solid rgba(128,178,255,.16);border-radius:16px;padding:14px}
135
+ .row{display:flex;gap:12px;flex-wrap:wrap}
136
+ .stat{flex:1 1 150px;background:#0f1b2e;border:1px solid rgba(128,178,255,.18);border-radius:12px;padding:10px}
137
+ .k{color:var(--muted);font-size:12px;margin-bottom:4px}.v{font-size:18px;font-weight:700}
138
+ .bars{display:flex;gap:8px}.bar{flex:1;background:#0f1b2e;border:1px solid rgba(128,178,255,.18);border-radius:12px;padding:10px}
139
+ .bar .lab{font-size:12px;color:var(--muted);margin-bottom:6px}.bar .w{height:14px;border-radius:10px;background:#173055;overflow:hidden}
140
+ .bar .f{height:100%;width:0%;transition:width .08s linear}.p0{background:var(--brown)}.p1{background:var(--green)}.p2{background:var(--gray)}
141
+ canvas{width:100%;height:260px;background:#0f1b2e;border:1px solid rgba(128,178,255,.18);border-radius:12px;display:block}
142
+ .hist{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-top:8px}
143
+ .cell{height:16px;border-radius:4px;opacity:.9}.a0{background:var(--green)}.a1{background:var(--orange)}.a2{background:var(--gray)}
144
+ .map{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
145
+ .map .m{font-size:11px;padding:4px 6px;border-radius:8px;border:1px solid rgba(128,178,255,.18);background:#0f1b2e}
146
+ .map .on{outline:2px solid var(--ring);background:rgba(59,130,246,.2)}
147
+ details{margin-top:10px}
148
+ .mono{font-family:ui-monospace,Consolas,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow:auto;max-height:480px;background:#0f1b2e;border:1px solid rgba(128,178,255,.18);padding:10px;border-radius:10px}
149
+ .foot{margin-top:10px;color:var(--muted);font-size:12px}
150
+ .vscan-legend{display:flex;gap:12px;margin-top:4px;font-size:12px;align-items:center}
151
+ .vscan-legend .dot{width:12px;height:12px;border-radius:3px}
152
+ </style>
153
+ <div class="wrap">
154
+ <h1>HCR — Obserwacje i Akcje</h1>
155
+ <p class="sub">Promienie/skany zwracają parę: <b>[długość, typ]</b>.</p>
156
+ <div class="grid">
157
+ <div class="card">
158
+ <div class="row">
159
+ <div class="stat"><div class="k">Kroki</div><div id="steps" class="v">0</div></div>
160
+ <div class="stat"><div class="k">Akcja</div><div id="cur" class="v">—</div></div>
161
+ <div class="stat"><div class="k">Nagroda</div><div id="rew" class="v">0.00</div></div>
162
+ <div class="stat"><div class="k">Dystans</div><div id="dst" class="v">0</div></div>
163
+ </div>
164
+ <div class="bars" style="margin-top:10px">
165
+ <div class="bar"><div class="lab">P(HAMULEC=0)</div><div class="w"><div id="p0" class="f p0"></div></div></div>
166
+ <div class="bar"><div class="lab">P(GAZ=1)</div><div class="w"><div id="p1" class="f p1"></div></div></div>
167
+ <div class="bar"><div class="lab">P(NIC=2)</div><div class="w"><div id="p2" class="f p2"></div></div></div>
168
+ </div>
169
+ <div style="margin-top:10px">
170
+ <div class="k">Historia akcji (ostatnie 5)</div>
171
+ <div id="hist" class="hist"></div>
172
+ </div>
173
+ <div style="margin-top:10px">
174
+ <div class="k">Mapy (one-hot)</div>
175
+ <div id="maps" class="map"></div>
176
+ </div>
177
+ <details open>
178
+ <summary>🔎 Surowe dane</summary>
179
+ <div class="mono" id="raw"></div>
180
+ </details>
181
+ </div>
182
+ <div class="card">
183
+ <div class="k">Skany pionowe — 1 = daleko, 0 = blisko</div>
184
+ <canvas id="vscan"></canvas>
185
+ <div class="vscan-legend">
186
+ <span style="display:flex;align-items:center;gap:4px"><div class="dot" style="background:var(--brown)"></div>Lina (Typ 0)</span>
187
+ <span style="display:flex;align-items:center;gap:4px"><div class="dot" style="background:var(--green)"></div>Ziemia (Typ 1)</span>
188
+ </div>
189
+ <div class="k" style="margin-top:10px">Lasery okrężne</div>
190
+ <canvas id="rays"></canvas>
191
+ </div>
192
+ </div>
193
+ <div class="foot">Typy trafień: 0 = <span style="color:var(--brown)">Lina (Brąz)</span>, 1 = <span style="color:var(--green)">Ziemia (Zielony)</span>. Brak trafienia: [1.0, 0.0]</div>
194
+ </div>
195
+ <script>
196
+ const $ = s => document.querySelector(s);
197
+ const steps=$('#steps'),cur=$('#cur'),rew=$('#rew'),dst=$('#dst');
198
+ const p0=$('#p0'),p1=$('#p1'),p2=$('#p2');
199
+ const hist=$('#hist'),maps=$('#maps'),raw=$('#raw');
200
+ const vscan=document.getElementById('vscan'), rays=document.getElementById('rays');
201
+ let mLabels = ['M1','M3','M4','M5','M7','M9','M11','M14','M16','M17','M18','M20','M25'];
202
+ function initHist(){ hist.innerHTML=''; for(let i=0;i<5;i++){ const d=document.createElement('div'); d.className='cell a2'; hist.appendChild(d);} }
203
+ function setHistIdx(arr){ const cells=hist.children; for(let i=0;i<Math.min(cells.length,arr.length);i++){ cells[i].className='cell a'+arr[i]; } }
204
+ function initMaps(){ maps.innerHTML=''; for(let i=0;i<mLabels.length;i++){ const s=document.createElement('span'); s.className='m'; s.textContent=mLabels[i]; maps.appendChild(s);} }
205
+ function setMaps(onehot){ const els=maps.children; for(let i=0;i<els.length;i++){ els[i].classList.toggle('on', (onehot[i]||0)>0.5); } }
206
+ function fitCanvas(c){ const dpr=window.devicePixelRatio||1; const rect=c.getBoundingClientRect(); c.width=Math.max(10,rect.width*dpr); c.height=Math.max(10,rect.height*dpr); const ctx=c.getContext('2d'); ctx.setTransform(dpr,0,0,dpr,0,0); return ctx; }
207
+ let vctx=null, rctx=null; function resize(){ vctx=fitCanvas(vscan); rctx=fitCanvas(rays); } window.addEventListener('resize', resize);
208
+ function setProb(el, p){ el.style.width = Math.round(100*(p||0))+'%'; }
209
+ function drawVScanBars(ctx, values){
210
+ const W=ctx.canvas.width, H=ctx.canvas.height; const n=values.length;
211
+ const pad=4; const bw=(W - pad*(n+1))/n; ctx.clearRect(0,0,W,H);
212
+ for(let i=0;i<n;i++){
213
+ const [dist, type] = values[i] || [1.0, 0.0];
214
+ const h = Math.max(1, (1.0 - dist) * H);
215
+ const x = pad + i * (bw + pad);
216
+ if (type === 0.0) ctx.fillStyle = '#a16207';
217
+ else if (type === 1.0) ctx.fillStyle = '#22c55e';
218
+ else continue;
219
+ ctx.fillRect(x, H - h, bw, h);
220
+ }
221
+ }
222
+ function drawRays(ctx, values){
223
+ const W=ctx.canvas.width, H=ctx.canvas.height; const cx=W/2, cy=H/2; const r=Math.min(W,H)*0.45;
224
+ ctx.clearRect(0,0,W,H); ctx.strokeStyle='#173055'; ctx.lineWidth=1; ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.stroke();
225
+ const n=values.length;
226
+ for(let i=0;i<n;i++){
227
+ const [dist, type] = values[i] || [1.0, 0.0];
228
+ if (dist >= 1.0) { ctx.strokeStyle = '#475569'; }
229
+ else if (type === 0.0) { ctx.strokeStyle = '#a16207'; }
230
+ else { ctx.strokeStyle = '#22c55e'; }
231
+ const ang=i/n*Math.PI*2; const rr=r*dist;
232
+ const x=cx+Math.cos(ang)*rr; const y=cy+Math.sin(ang)*rr;
233
+ ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(x,y); ctx.stroke();
234
+ }
235
+ }
236
+ function spanNum(n) {
237
+ const num = Number(n);
238
+ if (!Number.isFinite(num)) return String(n);
239
+ if (num >= 0 && num <= 1) {
240
+ const hue = 120 * (1 - num);
241
+ return `<span style="color:hsl(${hue}, 85%, 65%)">${num.toFixed(3)}</span>`;
242
+ }
243
+ return num.toFixed(3);
244
+ }
245
+ function coloredArray(arr){
246
+ if (!Array.isArray(arr)) return String(arr); let out='[';
247
+ for (let i=0;i<arr.length;i++){
248
+ if (Array.isArray(arr[i])) { out += '[' + spanNum(arr[i][0]) + ', ' + spanNum(arr[i][1]) + ']'; }
249
+ else { out += spanNum(arr[i]); }
250
+ if (i!==arr.length-1) out+=', ';
251
+ } out+=']'; return out;
252
+ }
253
+ function renderRaw(d){
254
+ const o=d.obs_parts||{};
255
+ raw.innerHTML =
256
+ 'map = ' + coloredArray(o.map||[]) + '<br>' +
257
+ 'rays[d,t] = ' + coloredArray(o.rays||[]) + '<br>' +
258
+ 'vscan[d,t] = ' + coloredArray(o.vscan||[]) + '<br>' +
259
+ 'angle/vel = ' + coloredArray(o.angle||[]) + ' ' + coloredArray(o.velocity||[]) + '<br>' +
260
+ 'action_hist = ' + coloredArray(o.action_history_idx||[]) + '<br>' +
261
+ 'action_probs = ' + coloredArray(d.action_probs||[]) + '<br>' +
262
+ 'flat(len='+(d.flat||[]).length+') = ' + coloredArray(d.flat||[]);
263
+ }
264
+ initHist(); initMaps(); resize();
265
+ async function tick(){
266
+ try{
267
+ const r = await fetch('/state', {cache:'no-store'});
268
+ const s = await r.json();
269
+ if (!s.obs_parts) return;
270
+ steps.textContent = s.step||0;
271
+ cur.textContent = ['HAMULEC','GAZ','NIC'][s.action] || s.action;
272
+ rew.textContent = (s.reward||0).toFixed(2);
273
+ dst.textContent = (s.distance||0);
274
+ const ap = s.action_probs || [0.33,0.33,0.34];
275
+ setProb(p0, ap[0]); setProb(p1, ap[1]); setProb(p2, ap[2]);
276
+ const op = s.obs_parts;
277
+ if (op.action_history_idx) setHistIdx(op.action_history_idx);
278
+ if (op.map) setMaps(op.map);
279
+ if (vctx && op.vscan) drawVScanBars(vctx, op.vscan);
280
+ if (rctx && op.rays) drawRays(rctx, op.rays);
281
+ renderRaw(s);
282
+ }catch(e){}
283
+ setTimeout(tick, 150);
284
+ }
285
+ tick();
286
+ </script>
287
+ </html>"""
288
+
289
+ class _Handler(BaseHTTPRequestHandler):
290
+ def _send(self, code=200, ctype="text/html; charset=utf-8"):
291
+ self.send_response(code); self.send_header("Content-Type", ctype); self.send_header("Cache-Control","no-store"); self.end_headers()
292
+ def log_message(self, *args, **kwargs): return
293
+ def do_GET(self):
294
+ if self.path in ("/", "/index.html"):
295
+ self._send(); self.wfile.write(_HTML.encode("utf-8")); return
296
+ if self.path == "/state":
297
+ with STATE_LOCK: payload = DASH_STATE
298
+ self._send(200, "application/json; charset=utf-8")
299
+ self.wfile.write(json.dumps(payload).encode("utf-8")); return
300
+ self._send(404, "text/plain; charset=utf-8"); self.wfile.write(b"404")
301
+
302
+ def start_dashboard(host="127.0.0.1", port=8088):
303
+ try:
304
+ srv = HTTPServer((host, port), _Handler)
305
+ t = threading.Thread(target=srv.serve_forever, daemon=True); t.start()
306
+ print(f"{Colors.GREEN}{Colors.BOLD}==> Podgląd: http://{host}:{srv.server_port}{Colors.RESET}")
307
+ except Exception as e:
308
+ print(f"{Colors.RED}Nie udało się uruchomić dashboardu: {e}{Colors.RESET}")
309
+
310
+ class StreamToLogger:
311
+ def __init__(self, logger, level): self.logger, self.level = logger, level
312
+ def write(self, buf):
313
+ for line in buf.rstrip().splitlines(): self.logger.log(self.level, line.rstrip())
314
+ def flush(self): pass
315
+
316
+ def setup_logging():
317
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s',
318
+ datefmt='%Y-%m-%d %H:%M:%S', handlers=[
319
+ logging.FileHandler(LOG_FILE_PATH, mode='a', encoding='utf-8'),
320
+ logging.StreamHandler(sys.__stdout__)])
321
+ sys.stdout = StreamToLogger(logging.getLogger('STDOUT'), logging.INFO)
322
+ sys.stderr = StreamToLogger(logging.getLogger('STDERR'), logging.ERROR)
323
+ logging.info("=" * 60 + "\nNOWA SESJA LOGOWANIA\n" + "=" * 60)
324
+
325
+ class MemoryReader:
326
+ def __init__(self, process_name):
327
+ self.process_name=process_name.lower(); self.pid=self._get_pid()
328
+ if not self.pid: raise RuntimeError(f"Nie znaleziono procesu: {self.process_name}")
329
+ self.handle=ctypes.windll.kernel32.OpenProcess(0x0010|0x0400|0x0020|0x0008, False, self.pid)
330
+ if not self.handle: raise RuntimeError(f"Nie można otworzyć procesu (PID: {self.pid}).")
331
+ self.module_base=self._get_module_base_address()
332
+ def _get_pid(self):
333
+ for p in psutil.process_iter(['pid','name']):
334
+ if p.info.get('name', '').lower()==self.process_name: return p.info['pid']
335
+ return None
336
+ def _get_module_base_address(self):
337
+ try:
338
+ for m in psutil.Process(self.pid).memory_maps(grouped=False):
339
+ if m.path and self.process_name in os.path.basename(m.path).lower(): return int(m.addr,16)
340
+ except psutil.Error: pass
341
+ raise RuntimeError(f"Nie można znaleźć adresu bazowego dla {self.process_name}")
342
+ def read_int(self, address):
343
+ buffer=(ctypes.c_byte*4)(); br=ctypes.c_size_t()
344
+ if address and ctypes.windll.kernel32.ReadProcessMemory(self.handle, ctypes.c_void_p(address), buffer, 4, ctypes.byref(br)):
345
+ if br.value==4: return struct.unpack('<I',bytes(buffer))[0]
346
+ return None
347
+ def write_int(self, address, value):
348
+ if address: ctypes.windll.kernel32.WriteProcessMemory(self.handle, address, ctypes.byref(ctypes.c_int(value)), 4, None)
349
+ def write_float(self, address, value):
350
+ if address: ctypes.windll.kernel32.WriteProcessMemory(self.handle, address, ctypes.byref(ctypes.c_float(value)), 4, None)
351
+ def get_final_address(self, base_offset, offsets):
352
+ try:
353
+ addr = self.module_base + base_offset
354
+ for off in offsets:
355
+ addr_val = self.read_int(addr)
356
+ if addr_val is None: return None
357
+ addr = addr_val + off
358
+ return addr
359
+ except: return None
360
+ def close(self):
361
+ if getattr(self,"handle",None): ctypes.windll.kernel32.CloseHandle(self.handle); self.handle=None
362
+
363
+ def ensure_game_is_running(process_name, aumid):
364
+ try: return MemoryReader(process_name)
365
+ except RuntimeError:
366
+ logging.warning(f"Nie znaleziono procesu gry. Próba uruchomienia...")
367
+ try:
368
+ subprocess.run(f'explorer.exe shell:appsFolder\\{aumid}', shell=True, timeout=15, check=False)
369
+ logging.info("Oczekiwanie 10 sekund na uruchomienie gry...")
370
+ time.sleep(10)
371
+ except Exception as e: logging.error(f"Błąd podczas uruchamiania gry: {e}")
372
+ for attempt in range(10):
373
+ try: return MemoryReader(process_name)
374
+ except RuntimeError: time.sleep(3)
375
+ raise RuntimeError("Nie udało się uruchomić i połączyć z grą po wielu próbach.")
376
+
377
+ def force_close_game(process_name):
378
+ try:
379
+ pyautogui.keyDown('alt'); pyautogui.press('f4'); pyautogui.keyUp('alt'); time.sleep(0.5)
380
+ except Exception: pass
381
+ for p in [p for p in psutil.process_iter(['name']) if p.info.get('name', '').lower()==process_name]:
382
+ try: p.terminate()
383
+ except: pass
384
+ time.sleep(1.0)
385
+ if any(p.info.get('name', '').lower()==process_name for p in psutil.process_iter(['name'])):
386
+ try: subprocess.run(f'taskkill /F /IM {process_name}', shell=True, check=False, timeout=5, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
387
+ except: pass
388
+ time.sleep(1.0)
389
+
390
+ def hard_restart_game(env, target_map_id=1):
391
+ logging.info("Wykonywanie twardego restartu gry...")
392
+ pyautogui.keyUp('left'); pyautogui.keyUp('right')
393
+ for attempt in range(1, 4):
394
+ logging.info(f"Próba restartu #{attempt}/3...")
395
+ if getattr(env, 'memory_reader', None): env.memory_reader.close()
396
+ force_close_game(PROCESS_NAME)
397
+ env.memory_reader = ensure_game_is_running(PROCESS_NAME, AUMID)
398
+ time.sleep(2.0)
399
+ if soft_start_race(env, target_map_id):
400
+ logging.info(f"Restart udany, wyścig rozpoczęty.")
401
+ return True
402
+ else:
403
+ logging.warning(f"Próba restartu #{attempt} nie powiodła się. Czekam 5 sekund przed kolejną próbą.")
404
+ time.sleep(5)
405
+ raise RuntimeError("Nie udało się poprawnie zrestartować gry i rozpocząć wyścigu po 3 próbach. Zatrzymuję skrypt.")
406
+
407
+ def soft_start_race(env, target_map_id=None) -> bool:
408
+ try:
409
+ if target_map_id is not None:
410
+ addr = env.memory_reader.get_final_address(MAP_POINTER_CONFIG["base_offset"], MAP_POINTER_CONFIG["offsets"])
411
+ if addr: env.memory_reader.write_int(addr, target_map_id)
412
+ try:
413
+ center_x = env.monitor['left'] + env.monitor['width'] // 2
414
+ center_y = env.monitor['top'] + env.monitor['height'] // 2
415
+ pyautogui.click(center_x, center_y)
416
+ time.sleep(0.2)
417
+ except Exception:
418
+ pass
419
+ pyautogui.click(MAP_SELECT_BUTTON); time.sleep(0.5)
420
+ pyautogui.click(START_RACE_BUTTON); time.sleep(1.0)
421
+ logging.info("Weryfikacja rozpoczęcia wyścigu...")
422
+ pixel_check_area = {'top': env.monitor['top'] + LIFE_CHECK_PIXEL_Y, 'left': env.monitor['left'] + LIFE_CHECK_PIXEL_X, 'width': 1, 'height': 1}
423
+ for i in range(30):
424
+ img = env.sct.grab(pixel_check_area)
425
+ if tuple(img.rgb) == LIFE_CHECK_EXACT_RGB:
426
+ logging.info("Weryfikacja pomyślna. Agent jest w grze.")
427
+ time.sleep(1.0)
428
+ return True
429
+ if i % 5 == 4:
430
+ pyautogui.click(START_RACE_BUTTON)
431
+ time.sleep(0.5)
432
+ logging.warning("Nie udało się zweryfikować startu wyścigu w ciągu 15 sekund.")
433
+ return False
434
+ except Exception as e:
435
+ logging.error(f"Wystąpił nieoczekiwany błąd podczas próby startu wyścigu: {e}", exc_info=True)
436
+ return False
437
+
438
+ def cast_ray(start_pos, angle_deg, max_dist, color_masks, shape):
439
+ rad = math.radians(angle_deg)
440
+ for i in range(1, max_dist):
441
+ x, y = int(start_pos[0] + i*math.cos(rad)), int(start_pos[1] + i*math.sin(rad))
442
+ if not (0 <= y < shape[0] and 0 <= x < shape[1]): return max_dist, TYPE_GROUND
443
+ if color_masks['rope'][y, x] > 0: return i, TYPE_ROPE
444
+ if color_masks['ground'][y, x] > 0: return i, TYPE_GROUND
445
+ return max_dist, TYPE_GROUND
446
+
447
+ def get_observation(masks, memory_reader, velocity_for_ai):
448
+ obs = []; map_features = [0.0]*NUM_MAP_FEATURES
449
+ if memory_reader:
450
+ addr=memory_reader.get_final_address(MAP_POINTER_CONFIG["base_offset"],MAP_POINTER_CONFIG["offsets"])
451
+ map_id=memory_reader.read_int(addr)
452
+ if map_id in MAP_IDS_TO_INDEX: map_features[MAP_IDS_TO_INDEX[map_id]]=1.0
453
+ obs.extend(map_features); vehicle_pos, vehicle_angle_deg = None, 0.0
454
+ cp,_=cv2.findContours(masks['pink'],cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
455
+ cy,_=cv2.findContours(masks['yellow'],cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
456
+ if cp and cy:
457
+ fc,bc=max(cp,key=cv2.contourArea),max(cy,key=cv2.contourArea); Mf,Mb=cv2.moments(fc),cv2.moments(bc)
458
+ if Mf["m00"]>0 and Mb["m00"]>0:
459
+ pf=(int(Mf["m10"]/Mf["m00"]),int(Mf["m01"]/Mf["m00"])); pb=(int(Mb["m10"]/Mb["m00"]),int(Mb["m01"]/Mb["m00"]))
460
+ vehicle_pos=((pb[0]+pf[0])//2,(pb[1]+pf[1])//2); vehicle_angle_deg=math.degrees(math.atan2(pf[1]-pb[1],pf[0]-pb[0]))
461
+ if vehicle_pos:
462
+ for angle in np.linspace(0,360,NUMBER_OF_RAYS,endpoint=False): dist,hit_type=cast_ray(vehicle_pos,angle,MAX_CAR_RAY_DISTANCE,masks,masks['pink'].shape); obs.extend([dist/MAX_CAR_RAY_DISTANCE,hit_type])
463
+ else:
464
+ for _ in range(NUMBER_OF_RAYS): obs.extend([1.0,TYPE_GROUND])
465
+ sp=masks['pink'].shape[1]/(NUM_VERTICAL_SCANS-1) if NUM_VERTICAL_SCANS > 1 else 0
466
+ for i in range(NUM_VERTICAL_SCANS):
467
+ sx=min(int(i*sp),masks['pink'].shape[1]-1); rope_hits,ground_hits=np.where(masks['rope'][:,sx]>0)[0],np.where(masks['ground'][:,sx]>0)[0]
468
+ first_rope_y=MAX_VERTICAL_RAY_DISTANCE if len(rope_hits)==0 else rope_hits[0]; first_ground_y=MAX_VERTICAL_RAY_DISTANCE if len(ground_hits)==0 else ground_hits[0]
469
+ dist,hit_type=(first_rope_y,TYPE_ROPE) if first_rope_y<=first_ground_y else (first_ground_y,TYPE_GROUND); obs.extend([dist/MAX_VERTICAL_RAY_DISTANCE,hit_type])
470
+ angle_rad=math.radians(vehicle_angle_deg)
471
+ obs.extend([0.5*(math.sin(angle_rad)+1.0),0.5*(math.cos(angle_rad)+1.0)])
472
+ obs.append(abs(math.tanh(velocity_for_ai / 30.0)))
473
+ return np.array(obs,dtype=np.float32), vehicle_pos
474
+
475
+ def determine_next_map(env):
476
+ try:
477
+ addr = env.memory_reader.get_final_address(MAP_POINTER_CONFIG["base_offset"], [])
478
+ current = env.memory_reader.read_int(addr) if addr else env.current_map_id
479
+ idx = MAP_ID_PROGRESSION.index(current) if current in MAP_ID_PROGRESSION else -1
480
+ next_map = MAP_ID_PROGRESSION[0] if idx == -1 or current == MAP_ID_PROGRESSION[-1] else MAP_ID_PROGRESSION[idx+1]
481
+ logging.info(f"Zmiana mapy z {current} na {next_map}")
482
+ return next_map
483
+ except: return 1
484
+
485
+ class HillClimbImitationEnv:
486
+ def __init__(self):
487
+ self.memory_reader = None
488
+ self.sct = mss.mss()
489
+ self.monitor = self.sct.monitors[MONITOR_INDEX]
490
+ self.capture_region = {"top": self.monitor["top"], "left": self.monitor["left"], "width": CAPTURE_RESOLUTION_WIDTH, "height": CAPTURE_RESOLUTION_HEIGHT}
491
+ pyautogui.FAILSAFE = False; pyautogui.PAUSE = 0.0
492
+ self.current_map_id = 1
493
+ self.last_distance = 0
494
+ self.last_step_time = time.time(); self.smoothed_velocity = 0.0
495
+ self.dist_address = None
496
+ self.fuel_address = None
497
+
498
+ def reacquire_pointers(self):
499
+ if not self.memory_reader: return False
500
+ logging.info("Ponowne wyszukiwanie wskaźników pamięci...")
501
+ self.dist_address = self.memory_reader.get_final_address(DISTANCE_POINTER_CONFIG["base_offset"], DISTANCE_POINTER_CONFIG["offsets"])
502
+ self.fuel_address = self.memory_reader.get_final_address(FUEL_POINTER_CONFIG["base_offset"], FUEL_POINTER_CONFIG["offsets"])
503
+ if self.dist_address and self.fuel_address:
504
+ logging.info("Wskaźniki znalezione pomyślnie.")
505
+ return True
506
+ logging.warning("Nie udało się ponownie znaleźć wskaźników.")
507
+ return False
508
+
509
+ def start(self):
510
+ pyautogui.keyUp('left'); pyautogui.keyUp('right')
511
+ try:
512
+ if not hard_restart_game(self, self.current_map_id):
513
+ return False
514
+ self.reacquire_pointers()
515
+ return True
516
+ except RuntimeError as e:
517
+ logging.critical(f"Krytyczny błąd startu: {e}", exc_info=True)
518
+ return False
519
+
520
+ def attach(self):
521
+ print(f"{Colors.YELLOW}Oczekiwanie na proces gry '{PROCESS_NAME}'...{Colors.RESET}")
522
+ while not INTERRUPT_REQUESTED.is_set():
523
+ try:
524
+ self.memory_reader = MemoryReader(PROCESS_NAME)
525
+ print(f"{Colors.GREEN}Gra znaleziona. Podłączono do procesu.{Colors.RESET}")
526
+ if self.reacquire_pointers():
527
+ return True
528
+ else:
529
+ print(f"{Colors.YELLOW}Nie znaleziono wskaźników. Prawdopodobnie jesteś w menu. Dalsze akcje wymagają rozpoczęcia gry.{Colors.RESET}")
530
+ return True
531
+ except RuntimeError:
532
+ time.sleep(2)
533
+ return False
534
+
535
+ def get_obs(self):
536
+ img = np.array(self.sct.grab(self.capture_region))
537
+ hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
538
+ masks = {'pink': cv2.inRange(hsv, LOWER_PINK, UPPER_PINK), 'yellow': cv2.inRange(hsv, LOWER_YELLOW, UPPER_YELLOW),
539
+ 'ground': cv2.inRange(hsv, LOWER_TARGET_GROUND, UPPER_TARGET_GROUND), 'rope': cv2.inRange(hsv, LOWER_ROPE, UPPER_ROPE)}
540
+
541
+ current_distance = self.memory_reader.read_int(self.dist_address) if self.dist_address else self.last_distance
542
+ time_delta = time.time() - self.last_step_time; self.last_step_time = time.time()
543
+ raw_velocity = (current_distance - self.last_distance) / time_delta if time_delta > 0.001 else 0.0
544
+ self.smoothed_velocity = VELOCITY_SMOOTHING_FACTOR * raw_velocity + (1 - VELOCITY_SMOOTHING_FACTOR) * self.smoothed_velocity
545
+ self.last_distance = current_distance
546
+
547
+ obs, _ = get_observation(masks, self.memory_reader, self.smoothed_velocity)
548
+ return obs
549
+
550
+ def get_state_for_ai(self):
551
+ img = np.array(self.sct.grab(self.capture_region))
552
+ img_bgr = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
553
+ hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
554
+ masks = {'pink': cv2.inRange(hsv, LOWER_PINK, UPPER_PINK), 'yellow': cv2.inRange(hsv, LOWER_YELLOW, UPPER_YELLOW),
555
+ 'ground': cv2.inRange(hsv, LOWER_TARGET_GROUND, UPPER_TARGET_GROUND), 'rope': cv2.inRange(hsv, LOWER_ROPE, UPPER_ROPE)}
556
+
557
+ current_distance = self.memory_reader.read_int(self.dist_address) if self.dist_address else None
558
+ if current_distance is None:
559
+ if self.reacquire_pointers():
560
+ current_distance = self.memory_reader.read_int(self.dist_address)
561
+ if current_distance is None:
562
+ current_distance = self.last_distance
563
+
564
+ time_delta = time.time() - self.last_step_time; self.last_step_time = time.time()
565
+ raw_velocity = (current_distance - self.last_distance) / time_delta if time_delta > 0.001 else 0.0
566
+ self.smoothed_velocity = VELOCITY_SMOOTHING_FACTOR * raw_velocity + (1 - VELOCITY_SMOOTHING_FACTOR) * self.smoothed_velocity
567
+ self.last_distance = current_distance
568
+
569
+ obs, vehicle_pos = get_observation(masks, self.memory_reader, self.smoothed_velocity)
570
+ return obs, img_bgr, current_distance, vehicle_pos
571
+
572
+ def perform_action(self, action):
573
+ if self.fuel_address: self.memory_reader.write_float(self.fuel_address, 100.0)
574
+ pyautogui.keyUp('left'); pyautogui.keyUp('right')
575
+ if action == 1: pyautogui.keyDown('right')
576
+ elif action == 0: pyautogui.keyDown('left')
577
+
578
+ def is_alive(self):
579
+ px_area = {'top': self.monitor['top'] + LIFE_CHECK_PIXEL_Y, 'left': self.monitor['left'] + LIFE_CHECK_PIXEL_X, 'width': 1, 'height': 1}
580
+ return tuple(self.sct.grab(px_area).rgb) == LIFE_CHECK_EXACT_RGB
581
+
582
+ def close(self):
583
+ if self.memory_reader: self.memory_reader.close()
584
+ self.sct.close(); pyautogui.keyUp('left'); pyautogui.keyUp('right')
585
+
586
+ class MLP(nn.Module):
587
+ def __init__(self, input_size, output_size, layer_sizes, dropout_rate):
588
+ super(MLP, self).__init__()
589
+ layers = []
590
+ in_size = input_size
591
+ for out_size in layer_sizes:
592
+ layers.append(nn.Linear(in_size, out_size))
593
+ layers.append(nn.ReLU())
594
+ layers.append(nn.Dropout(p=dropout_rate))
595
+ in_size = out_size
596
+ layers.append(nn.Linear(in_size, output_size))
597
+ self.network = nn.Sequential(*layers)
598
+
599
+ def forward(self, x):
600
+ return self.network(x)
601
+
602
+ def draw_ai_pov(image, obs, action, distance, vehicle_pos):
603
+ font = cv2.FONT_HERSHEY_SIMPLEX
604
+ font_scale_small = 0.4
605
+ text_color = (50, 255, 255)
606
+
607
+ if vehicle_pos:
608
+ map_features_offset = NUM_MAP_FEATURES
609
+ rays_data = obs[map_features_offset : map_features_offset + (NUMBER_OF_RAYS * 2)]
610
+
611
+ for i in range(NUMBER_OF_RAYS):
612
+ norm_dist, hit_type = rays_data[i*2], rays_data[i*2+1]
613
+ dist = norm_dist * MAX_CAR_RAY_DISTANCE
614
+ angle_deg = np.linspace(0, 360, NUMBER_OF_RAYS, endpoint=False)[i]
615
+ angle_rad = math.radians(angle_deg)
616
+
617
+ end_x = int(vehicle_pos[0] + dist * math.cos(angle_rad))
618
+ end_y = int(vehicle_pos[1] + dist * math.sin(angle_rad))
619
+
620
+ color = (0, 255, 0) if hit_type == TYPE_GROUND else (255, 100, 100)
621
+ cv2.line(image, vehicle_pos, (end_x, end_y), color, 2)
622
+
623
+ cv2.circle(image, (end_x, end_y), 4, color, -1)
624
+
625
+ cv2.putText(image, f"{norm_dist:.2f}", (end_x + 6, end_y + 4), font, font_scale_small, text_color, 1, cv2.LINE_AA)
626
+
627
+ vscans_offset = NUM_MAP_FEATURES + (NUMBER_OF_RAYS * 2)
628
+ vscans_data = obs[vscans_offset : vscans_offset + (NUM_VERTICAL_SCANS * 2)]
629
+
630
+ img_height, img_width, _ = image.shape
631
+
632
+ for i in range(NUM_VERTICAL_SCANS):
633
+ norm_dist, hit_type = vscans_data[i*2], vscans_data[i*2+1]
634
+ dist = norm_dist * MAX_VERTICAL_RAY_DISTANCE
635
+
636
+ start_x = int(i * (img_width - 1) / (NUM_VERTICAL_SCANS - 1)) if NUM_VERTICAL_SCANS > 1 else img_width // 2
637
+ end_y = int(dist)
638
+
639
+ if end_y < img_height:
640
+ color = (0, 200, 0) if hit_type == TYPE_GROUND else (200, 50, 50)
641
+ cv2.line(image, (start_x, 0), (start_x, end_y), color, 1)
642
+ cv2.circle(image, (start_x, end_y), 4, color, -1)
643
+ cv2.putText(image, f"{norm_dist:.2f}", (start_x + 6, end_y - 6), font, font_scale_small, text_color, 1, cv2.LINE_AA)
644
+
645
+ if vehicle_pos:
646
+ angle_offset = vscans_offset + (NUM_VERTICAL_SCANS * 2)
647
+ angle_data = obs[angle_offset : angle_offset + NUM_ANGLE_FEATURES]
648
+
649
+ sin_norm, cos_norm = angle_data[0], angle_data[1]
650
+ sin_val = (sin_norm * 2) - 1
651
+ cos_val = (cos_norm * 2) - 1
652
+
653
+ dir_end_x = int(vehicle_pos[0] + 120 * cos_val)
654
+ dir_end_y = int(vehicle_pos[1] + 120 * sin_val)
655
+ cv2.arrowedLine(image, vehicle_pos, (dir_end_x, dir_end_y), (0, 255, 255), 3, tipLength=0.2)
656
+
657
+ cos_len, sin_len = 80 * cos_val, 80 * sin_val
658
+ cv2.line(image, vehicle_pos, (int(vehicle_pos[0] + cos_len), vehicle_pos[1]), (255, 0, 255), 2)
659
+ cv2.line(image, (int(vehicle_pos[0] + cos_len), vehicle_pos[1]), (int(vehicle_pos[0] + cos_len), int(vehicle_pos[1] + sin_len)), (0, 165, 255), 2)
660
+
661
+ cv2.rectangle(image, (5, 5), (450, 85), (0, 0, 0), -1)
662
+
663
+ dist_text = f"Dystans: {distance or 0} m"
664
+ cv2.putText(image, dist_text, (10, 35), font, 1, (255, 255, 255), 2, cv2.LINE_AA)
665
+
666
+ action_map_text = {0: "HAMULEC / LEWO", 1: "GAZ / PRAWO", 2: "NIC"}
667
+ action_text = f"Akcja: {action_map_text.get(action, 'N/A')}"
668
+ cv2.putText(image, action_text, (10, 70), font, 1, (255, 255, 255), 2, cv2.LINE_AA)
669
+
670
+ return image
671
+
672
+
673
+ def display_dashboard_for_recording(obs, current_action, sample_count):
674
+ reset_cursor_position()
675
+
676
+ action_map_text = {0: "HAMULEC", 1: "GAZ", 2: "NIC"}
677
+ action_text = action_map_text[current_action]
678
+
679
+ def get_color(value, is_dist=True):
680
+ if not is_dist:
681
+ if value > 0.6: return Colors.GREEN
682
+ if value > 0.4: return Colors.YELLOW
683
+ return Colors.CYAN
684
+ if value > 0.9: return Colors.GREEN
685
+ if value > 0.5: return Colors.YELLOW
686
+ if value > 0.1: return Colors.CYAN
687
+ return Colors.RED
688
+
689
+ sys.__stdout__.write(f"{Colors.BOLD}{Colors.CYAN}{'--- NAGRYWANIE DEMONSTRACJI ---':^80}{Colors.RESET}\n")
690
+ sys.__stdout__.write(f" {Colors.BOLD}Zapisanych obserwacji: {Colors.GREEN}{sample_count: >5}{Colors.RESET} | {Colors.BOLD}Twoja akcja: {Colors.GREEN}{action_text: >7}{Colors.RESET}\n\n")
691
+
692
+ sys.__stdout__.write(f"{Colors.BOLD}{Colors.YELLOW}[ PRZESTRZEŃ OBSERWACJI (to co widzi AI) ]{Colors.RESET}\n")
693
+
694
+ i=0
695
+ map_vec = obs[i:i+NUM_MAP_FEATURES]
696
+ map_idx = np.argmax(map_vec) if np.sum(map_vec) > 0 else -1
697
+ map_str = "".join([f"{Colors.GREEN}{Colors.BOLD}1{Colors.RESET}" if j==map_idx else "0" for j in range(NUM_MAP_FEATURES)])
698
+ sys.__stdout__.write(f" - {Colors.BOLD}Mapa ({map_idx if map_idx != -1 else '??'}): [{map_str}]\n")
699
+ i += NUM_MAP_FEATURES
700
+
701
+ i_rays_end = i + (NUMBER_OF_RAYS * 2)
702
+ i_vscan_end = i_rays_end + (NUM_VERTICAL_SCANS * 2)
703
+ angle_vec = obs[i_vscan_end : i_vscan_end + NUM_ANGLE_FEATURES]
704
+ vel_vec = obs[i_vscan_end + NUM_ANGLE_FEATURES : i_vscan_end + NUM_ANGLE_FEATURES + NUM_VELOCITY_FEATURES]
705
+
706
+ sin_val = (angle_vec[0] * 2) - 1
707
+ cos_val = (angle_vec[1] * 2) - 1
708
+ angle_deg = math.degrees(math.atan2(sin_val, cos_val))
709
+ sys.__stdout__.write(f" - {Colors.BOLD}Kąt: {angle_deg: >6.1f}°{Colors.RESET} (sin: {get_color(angle_vec[0],0)}{angle_vec[0]:.3f}{Colors.RESET}, cos: {get_color(angle_vec[1],0)}{angle_vec[1]:.3f}{Colors.RESET})\n")
710
+ sys.__stdout__.write(f" - {Colors.BOLD}Prędkość (norm): {get_color(vel_vec[0],0)}{vel_vec[0]:.3f}{Colors.RESET}\n\n")
711
+
712
+ sys.__stdout__.write(f" - {Colors.BOLD}Promienie ({NUMBER_OF_RAYS}):{Colors.RESET}\n")
713
+ for k in range(NUMBER_OF_RAYS):
714
+ dist, type = obs[i+k*2], obs[i+k*2+1]
715
+ color = get_color(dist)
716
+ type_char = "L" if type == TYPE_ROPE else "Z"
717
+ sys.__stdout__.write(f" {color}{dist:.2f}{Colors.RESET}{type_char} ")
718
+ if (k + 1) % 13 == 0: sys.__stdout__.write("\n")
719
+ sys.__stdout__.write("\n\n")
720
+ i = i_rays_end
721
+
722
+ sys.__stdout__.write(f" - {Colors.BOLD}Skany pionowe ({NUM_VERTICAL_SCANS}):{Colors.RESET}\n")
723
+ for k in range(NUM_VERTICAL_SCANS):
724
+ dist, type = obs[i+k*2], obs[i+k*2+1]
725
+ color = get_color(dist)
726
+ type_char = "L" if type == TYPE_ROPE else "Z"
727
+ sys.__stdout__.write(f" {color}{dist:.2f}{Colors.RESET}{type_char} ")
728
+ sys.__stdout__.write("\n")
729
+
730
+ sys.__stdout__.flush()
731
+
732
+ def record_demonstrations():
733
+ if not keyboard:
734
+ print(f"{Colors.RED}Biblioteka 'keyboard' nie jest zainstalowana. Nagrywanie niemożliwe.{Colors.RESET}")
735
+ return
736
+
737
+ print(f"\n{Colors.CYAN}--- Tryb nagrywania demonstracji ---{Colors.RESET}")
738
+ print(f"{Colors.YELLOW}1. Uruchom grę Hill Climb Racing.{Colors.RESET}")
739
+ print(f"{Colors.YELLOW}2. Rozpocznij dowolny wyścig.{Colors.RESET}")
740
+ print(f"{Colors.YELLOW}Program automatycznie rozpocznie nagrywanie, gdy wykryje, że jesteś w grze.{Colors.RESET}")
741
+ print(f"Naciśnij 'Q', aby w dowolnym momencie zakończyć nagrywanie i zapisać dane.")
742
+
743
+ env = HillClimbImitationEnv()
744
+ if not env.attach():
745
+ print(f"{Colors.RED}Nie udało się podłączyć do gry. Zamykanie trybu nagrywania.{Colors.RESET}"); env.close(); return
746
+
747
+ print(f"\n{Colors.YELLOW}Oczekiwanie na rozpoczęcie wyścigu...{Colors.RESET}")
748
+ while not env.is_alive():
749
+ if keyboard.is_pressed('q'): print("Anulowano."); env.close(); return
750
+ time.sleep(0.5)
751
+
752
+ print(f"{Colors.GREEN}Wykryto aktywny wyścig. Rozpoczynam nagrywanie!{Colors.RESET}"); time.sleep(1)
753
+
754
+ demonstration_buffer = []; alive = True
755
+ hide_cursor()
756
+ try:
757
+ while not keyboard.is_pressed('q'):
758
+ if not alive:
759
+ reset_cursor_position(); print(f"{Colors.RED}{Colors.BOLD}ITO! Usuwam ostatnie 10 sekund nagrania...{Colors.RESET}\n")
760
+ current_time = time.time()
761
+ initial_count = len(demonstration_buffer)
762
+ demonstration_buffer = [d for d in demonstration_buffer if current_time - d[2] > 10]
763
+ removed_count = initial_count - len(demonstration_buffer)
764
+ print(f"Usunięto {removed_count} obserwacji. Pozostało: {len(demonstration_buffer)}.\n")
765
+ print(f"{Colors.YELLOW}Nagrywanie wstrzymane. Rozpocznij nowy wyścig, aby kontynuować...{Colors.RESET} (Naciśnij Q, aby zakończyć)")
766
+
767
+ while not env.is_alive():
768
+ if keyboard.is_pressed('q'): break
769
+ time.sleep(0.5)
770
+ if keyboard.is_pressed('q'): break
771
+
772
+ print(f"{Colors.GREEN}Wznowiono nagrywanie! Ponowne wyszukiwanie wskaźników...{Colors.RESET}")
773
+ while not env.reacquire_pointers():
774
+ if keyboard.is_pressed('q'): break
775
+ print(f"{Colors.YELLOW}Próba ponownego znalezienia wskaźników...{Colors.RESET}")
776
+ time.sleep(1)
777
+ if keyboard.is_pressed('q'): break
778
+
779
+ alive = True; pyautogui.keyUp('left'); pyautogui.keyUp('right'); continue
780
+
781
+ if env.fuel_address: env.memory_reader.write_float(env.fuel_address, 100.0)
782
+
783
+ obs = env.get_obs()
784
+ current_action = 2
785
+ if keyboard.is_pressed('right arrow'): current_action = 1
786
+ elif keyboard.is_pressed('left arrow'): current_action = 0
787
+
788
+ display_dashboard_for_recording(obs, current_action, len(demonstration_buffer))
789
+ demonstration_buffer.append((obs, current_action, time.time()))
790
+
791
+ alive = env.is_alive()
792
+ time.sleep(0.05)
793
+
794
+ except Exception as e:
795
+ logging.error(f"Wystąpił błąd podczas nagrywania: {e}", exc_info=True)
796
+ finally:
797
+ show_cursor()
798
+ env.close()
799
+
800
+ if demonstration_buffer:
801
+ print(f"\n{Colors.GREEN}Zapisywanie {len(demonstration_buffer)} obserwacji...{Colors.RESET}")
802
+ observations, actions, _ = zip(*demonstration_buffer)
803
+
804
+ if DEMONSTRATIONS_PATH.exists():
805
+ print("Znaleziono istniejący plik. Łączenie danych...")
806
+ with np.load(DEMONSTRATIONS_PATH) as data:
807
+ old_obs, old_actions = data['observations'], data['actions']
808
+ observations = np.concatenate((old_obs, np.array(observations)))
809
+ actions = np.concatenate((old_actions, np.array(actions)))
810
+ print(f"Połączono. Łączna liczba obserwacji: {len(observations)}.")
811
+
812
+ np.savez_compressed(DEMONSTRATIONS_PATH, observations=np.array(observations), actions=np.array(actions))
813
+ print(f"Dane zapisano pomyślnie w '{DEMONSTRATIONS_PATH.name}'.")
814
+ else:
815
+ print(f"{Colors.YELLOW}Nie zebrano żadnych nowych danych do zapisu.{Colors.RESET}")
816
+
817
+ def objective(trial: optuna.Trial, observations, actions):
818
+ n_layers = trial.suggest_int("n_layers", 1, 4)
819
+ layer_sizes = [trial.suggest_categorical(f"n_units_l{i}", [64, 128, 256, 512]) for i in range(n_layers)]
820
+ dropout_rate = trial.suggest_float("dropout", 0.1, 0.5)
821
+ learning_rate = trial.suggest_float("lr", 1e-5, 1e-2, log=True)
822
+ optimizer_name = trial.suggest_categorical("optimizer", ["Adam", "AdamW", "RMSprop", "SGD"])
823
+ batch_size = trial.suggest_categorical("batch_size", [32, 64, 128, 256])
824
+ epochs = OPTUNA_EPOCHS_PER_TRIAL
825
+
826
+ device = "cuda" if torch.cuda.is_available() else "cpu"
827
+ X_train, X_val, y_train, y_val = train_test_split(observations, actions, test_size=0.2, random_state=42, stratify=actions)
828
+ train_dataset = TensorDataset(torch.from_numpy(X_train).float(), torch.from_numpy(y_train).long())
829
+ val_dataset = TensorDataset(torch.from_numpy(X_val).float(), torch.from_numpy(y_val).long())
830
+ train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
831
+ val_loader = DataLoader(val_dataset, batch_size=batch_size)
832
+
833
+ model = MLP(OBS_SIZE, 3, layer_sizes, dropout_rate).to(device)
834
+ optimizer = getattr(optim, optimizer_name)(model.parameters(), lr=learning_rate)
835
+ criterion = nn.CrossEntropyLoss()
836
+
837
+ for epoch in range(epochs):
838
+ model.train()
839
+ for batch_X, batch_y in train_loader:
840
+ batch_X, batch_y = batch_X.to(device), batch_y.to(device)
841
+ optimizer.zero_grad(); outputs = model(batch_X); loss = criterion(outputs, batch_y)
842
+ loss.backward(); optimizer.step()
843
+
844
+ model.eval()
845
+ correct, total = 0, 0
846
+ with torch.no_grad():
847
+ for batch_X, batch_y in val_loader:
848
+ batch_X, batch_y = batch_X.to(device), batch_y.to(device)
849
+ outputs = model(batch_X); _, predicted = torch.max(outputs.data, 1)
850
+ total += batch_y.size(0); correct += (predicted == batch_y).sum().item()
851
+
852
+ accuracy = correct / total
853
+ trial.report(accuracy, epoch)
854
+ if trial.should_prune(): raise optuna.exceptions.TrialPruned()
855
+ return accuracy
856
+
857
+ def run_optuna_optimization():
858
+ if not DEMONSTRATIONS_PATH.exists():
859
+ print(f"{Colors.RED}Nie znaleziono pliku z danymi '{DEMONSTRATIONS_PATH.name}'. Najpierw nagraj demonstracje.{Colors.RESET}"); return
860
+
861
+ with np.load(DEMONSTRATIONS_PATH) as data:
862
+ observations, actions = data['observations'], data['actions']
863
+ print(f"Załadowano {len(observations)} obserwacji do optymalizacji.")
864
+
865
+ study = optuna.create_study(study_name="hcr-imitation-study", storage=f"sqlite:///{OPTUNA_DB_PATH}", load_if_exists=True, direction="maximize")
866
+
867
+ try:
868
+ study.optimize(lambda trial: objective(trial, observations, actions), n_trials=OPTUNA_TRIALS)
869
+ except KeyboardInterrupt:
870
+ print(f"\n{Colors.YELLOW}Optymalizacja przerwana przez użytkownika.{Colors.RESET}")
871
+
872
+ complete_trials = study.get_trials(deepcopy=False, states=[optuna.trial.TrialState.COMPLETE])
873
+ print(f"\n--- Podsumowanie Optymalizacji ---")
874
+ print(f"Liczba prób: {len(study.trials)}")
875
+
876
+ if complete_trials:
877
+ print(f"\n{Colors.GREEN}Najlepsza próba:{Colors.RESET}")
878
+ best_trial = study.best_trial
879
+ print(f" Wartość (celność): {best_trial.value:.4f}")
880
+ print(" Parametry:")
881
+ for key, value in best_trial.params.items(): print(f" - {key}: {value}")
882
+
883
+ print(f"\n{Colors.CYAN}Trenowanie finalnego modelu z najlepszymi parametrami...{Colors.RESET}")
884
+ device = "cuda" if torch.cuda.is_available() else "cpu"; params = best_trial.params
885
+ n_layers = params["n_layers"]; layer_sizes = [params[f"n_units_l{i}"] for i in range(n_layers)]
886
+ final_model = MLP(OBS_SIZE, 3, layer_sizes, params["dropout"]).to(device)
887
+ optimizer = getattr(optim, params["optimizer"])(final_model.parameters(), lr=params["lr"])
888
+ criterion = nn.CrossEntropyLoss()
889
+ full_dataset = TensorDataset(torch.from_numpy(observations).float(), torch.from_numpy(actions).long())
890
+ train_loader = DataLoader(full_dataset, batch_size=params["batch_size"], shuffle=True)
891
+
892
+ final_model.train()
893
+ for epoch in tqdm(range(FINAL_MODEL_EPOCHS), desc="Finalny trening"):
894
+ for batch_X, batch_y in train_loader:
895
+ batch_X, batch_y = batch_X.to(device), batch_y.to(device)
896
+ optimizer.zero_grad(); outputs = final_model(batch_X); loss = criterion(outputs, batch_y)
897
+ loss.backward(); optimizer.step()
898
+
899
+ torch.save(final_model.state_dict(), MODEL_SAVE_PATH)
900
+ print(f"{Colors.GREEN}Finalny model został wytrenowany i zapisany w '{MODEL_SAVE_PATH.name}'.{Colors.RESET}")
901
+ else:
902
+ print(f"{Colors.YELLOW}Nie ukończono żadnej próby. Nie można wyłonić najlepszego modelu.{Colors.RESET}")
903
+
904
+ def play_with_model():
905
+ if not MODEL_SAVE_PATH.exists():
906
+ print(f"{Colors.RED}Nie znaleziono pliku modelu '{MODEL_SAVE_PATH.name}'. Najpierw go wytrenuj.{Colors.RESET}"); return
907
+
908
+ start_dashboard()
909
+ print("\nTrwa ładowanie modelu i przygotowywanie środowiska...")
910
+ try:
911
+ study = optuna.load_study(study_name="hcr-imitation-study", storage=f"sqlite:///{OPTUNA_DB_PATH}")
912
+ params = study.best_trial.params
913
+ n_layers = params["n_layers"]; layer_sizes = [params[f"n_units_l{i}"] for i in range(n_layers)]
914
+ dropout = params["dropout"]
915
+ print(f"{Colors.GREEN}Załadowano architekturę z najlepszej próby Optuny.{Colors.RESET}")
916
+ except Exception:
917
+ print(f"{Colors.YELLOW}Nie udało się wczytać architektury z bazy Optuny. Używam domyślnej architektury.[256, 128]{Colors.RESET}")
918
+ layer_sizes = [256, 128]; dropout = 0.3
919
+
920
+ device = "cuda" if torch.cuda.is_available() else "cpu"
921
+ model = MLP(OBS_SIZE, 3, layer_sizes, dropout).to(device)
922
+ model.load_state_dict(torch.load(MODEL_SAVE_PATH)); model.eval()
923
+ print(f"Model załadowany i uruchomiony na: {device.upper()}")
924
+
925
+ env = HillClimbImitationEnv()
926
+
927
+ print(f"\n{Colors.CYAN}AI przejmuje kontrolę za:{Colors.RESET}")
928
+ for i in range(3, 0, -1):
929
+ sys.__stdout__.write(f"\r{Colors.BOLD}{Colors.YELLOW}... {i}{Colors.RESET}")
930
+ time.sleep(1)
931
+ sys.__stdout__.write(f"\r{Colors.BOLD}{Colors.GREEN}AI AKTYWNA!{Colors.RESET} Naciśnij Ctrl+C, aby zakończyć.\n")
932
+
933
+ hide_cursor()
934
+ total_steps = 0
935
+ try:
936
+ if not env.attach():
937
+ raise RuntimeError("Nie udało się podłączyć do procesu gry na starcie.")
938
+
939
+ cv2.namedWindow('Podglad AI', cv2.WINDOW_NORMAL)
940
+ cv2.resizeWindow('Podglad AI', 960, 540)
941
+
942
+ while not INTERRUPT_REQUESTED.is_set():
943
+ if not env.is_alive():
944
+ print(f"{Colors.YELLOW}AI nie jest w grze. Uruchamianie procedury startowej...{Colors.RESET}")
945
+ if not env.start():
946
+ print(f"{Colors.RED}Nie udało się uruchomić/zrestartować gry. Zatrzymuję.{Colors.RESET}")
947
+ break
948
+ else:
949
+ print(f"{Colors.GREEN}Wykryto aktywną grę. AI przejmuje kontrolę.{Colors.RESET}")
950
+
951
+ alive = True
952
+ do_hard_restart = False
953
+ max_distance_in_episode = -1.0
954
+ last_max_dist_time = time.time()
955
+ STAGNATION_SECONDS = 60.0
956
+
957
+ while alive:
958
+ obs, img, distance, vehicle_pos = env.get_state_for_ai()
959
+
960
+ if max_distance_in_episode < 0:
961
+ max_distance_in_episode = distance if distance is not None else 0.0
962
+
963
+ current_dist = distance if distance is not None else max_distance_in_episode
964
+ if current_dist > max_distance_in_episode:
965
+ max_distance_in_episode = current_dist
966
+ last_max_dist_time = time.time()
967
+ elif (time.time() - last_max_dist_time) > STAGNATION_SECONDS:
968
+ print(f"{Colors.RED}Wykryto stagnację! Dystans ({int(current_dist)}m) nie zwiększył się od {int(STAGNATION_SECONDS)}s. Wymuszam twardy restart gry...{Colors.RESET}")
969
+ do_hard_restart = True
970
+ break
971
+
972
+ with torch.no_grad():
973
+ obs_tensor = torch.from_numpy(obs).float().unsqueeze(0).to(device)
974
+ outputs = model(obs_tensor)
975
+ probs = torch.softmax(outputs, dim=1).squeeze().tolist()
976
+ action = torch.argmax(outputs, dim=1).item()
977
+
978
+ total_steps += 1
979
+ publish_to_dashboard(obs, action, probs, 0.0, {'distance': distance}, total_steps)
980
+
981
+ display_img = draw_ai_pov(img.copy(), obs, action, distance, vehicle_pos)
982
+ cv2.imshow('Podglad AI', display_img)
983
+ if cv2.waitKey(1) & 0xFF == ord('q'):
984
+ INTERRUPT_REQUESTED.set()
985
+ break
986
+
987
+ env.perform_action(action)
988
+ alive = env.is_alive()
989
+ time.sleep(0.02)
990
+
991
+ if INTERRUPT_REQUESTED.is_set(): break
992
+
993
+ print(f"{Colors.YELLOW}AI zginęła. Przechodzenie do następnej mapy...{Colors.RESET}")
994
+ next_map_id = determine_next_map(env)
995
+ env.current_map_id = next_map_id
996
+ if do_hard_restart:
997
+ try:
998
+ hard_restart_game(env, next_map_id)
999
+ except Exception as e:
1000
+ logging.error(f"Twardy restart po stagnacji zawiódł: {e}")
1001
+ break
1002
+ else:
1003
+ if not soft_start_race(env, next_map_id):
1004
+ logging.warning("Miękki start nie powiódł się. Próba twardego restartu...")
1005
+ try:
1006
+ hard_restart_game(env, next_map_id)
1007
+ except Exception as e:
1008
+ logging.error(f"Twardy restart zawiódł: {e}")
1009
+ break
1010
+ env.reacquire_pointers()
1011
+ env.last_distance = 0
1012
+ env.smoothed_velocity = 0.0
1013
+ env.last_step_time = time.time()
1014
+ time.sleep(1.0)
1015
+
1016
+ except (KeyboardInterrupt, RuntimeError) as e:
1017
+ if isinstance(e, RuntimeError):
1018
+ print(f"{Colors.RED}Wystąpił błąd krytyczny: {e}{Colors.RESET}")
1019
+ print("\nZakończono grę.")
1020
+ finally:
1021
+ show_cursor()
1022
+ cv2.destroyAllWindows()
1023
+ env.close()
1024
+
1025
+ def main_menu():
1026
+ if not all([torch, keyboard, optuna, tqdm, train_test_split]):
1027
+ print(f"\n{Colors.RED}Brak wymaganych bibliotek. Zainstaluj je i uruchom program ponownie.{Colors.RESET}"); return
1028
+
1029
+ while True:
1030
+ sys.__stdout__.write(f"\n{Colors.CYAN}{Colors.BOLD}{'--- MENU GŁÓWNE - UCZENIE PRZEZ IMITACJĘ ---':^60}{Colors.RESET}\n"
1031
+ f" {Colors.YELLOW}1.{Colors.RESET} Graj na wszystkich mapach (Tryb AI z podglądem)\n"
1032
+ f" {Colors.RED}2.{Colors.RESET} Wyjdź\n")
1033
+ choice = input(f"{Colors.BOLD}Wybierz opcję (1-2): {Colors.RESET}").strip()
1034
+ if choice == '1':
1035
+ play_with_model()
1036
+ elif choice == '2':
1037
+ print(f"{Colors.CYAN}Do zobaczenia!{Colors.RESET}")
1038
+ break
1039
+ else:
1040
+ print(f"{Colors.RED}Nieprawidłowy wybór.{Colors.RESET}")
1041
+
1042
+ if __name__ == "__main__":
1043
+ if sys.platform == "win32": ctypes.windll.kernel32.SetConsoleTitleW("HCR AI Control - Imitation Learning")
1044
+ setup_logging()
1045
+ try:
1046
+ main_menu()
1047
+ except Exception:
1048
+ logging.critical("KRYTYCZNY, NIEZŁAPANY BŁĄD NA NAJWYŻSZYM POZIOMIE!", exc_info=True)
1049
+ sys.__stderr__.write(f"{Colors.RED}Wystąpił krytyczny błąd. Sprawdź plik logu.{Colors.RESET}\n")
1050
+ finally:
1051
+ show_cursor()
1052
+ logging.info("Zakończono działanie programu.")
hcr_imitation_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5817bd299cbcb019b5b42c7d2a11fb6f00fcbccfda09c58676657c7048dcb164
3
+ size 1434549