Upload 2 files
Browse files- ai.py +1052 -0
- 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
|