diff --git a/src/03_simulation_volume.py b/src/03_simulation_volume.py new file mode 100644 index 0000000..a3cdb98 --- /dev/null +++ b/src/03_simulation_volume.py @@ -0,0 +1,333 @@ +import sys +import random +import numpy as np +import pygame +import sounddevice as sd + +# ==================== KONFIGURATION & PARAMETER ==================== +ROOM_SIZE_M = 20.0 +WINDOW_SIZE = 800 +PIXELS_PER_METER = WINDOW_SIZE / ROOM_SIZE_M + +SAMPLE_RATE = 44100 +BLOCK_SIZE = 1024 +C_SOUND = 343.0 +HEAD_RADIUS_M = 0.0875 +MAX_MAPPED_DIST = 10.0 + +# Schwellenwert in Metern für die Verbindung/Fusion von Punkten zu einem Körper +CLUSTER_THRESHOLD_M = 1.8 + +# Musikalische Intervalle zur Unterscheidung verschiedener fused Körper +CHORD_RATIOS = [1.0, 1.2, 1.498, 1.782, 2.0, 2.4] + + +# ==================== PUNKTE (POINT OBJECTS) ==================== +class MovingPoint: + """Repräsentiert einen einzelnen physikalischen Punkt im Raum.""" + def __init__(self, x_m, y_m): + self.x_m = x_m + self.y_m = y_m + self.vx = random.uniform(-0.025, 0.025) + self.vy = random.uniform(-0.025, 0.025) + + def update_physics(self): + """Autonome Bewegung und Kollision mit den Raumgrenzen.""" + self.x_m += self.vx + self.y_m += self.vy + + self.vx += random.uniform(-0.001, 0.001) + self.vy += random.uniform(-0.001, 0.001) + + speed = np.sqrt(self.vx**2 + self.vy**2) + if speed > 0.04: + self.vx = (self.vx / speed) * 0.04 + self.vy = (self.vy / speed) * 0.04 + + half_r = ROOM_SIZE_M / 2.0 - 0.5 + if abs(self.x_m) > half_r: + self.vx *= -1.0 + self.x_m = np.clip(self.x_m, -half_r, half_r) + if abs(self.y_m) > half_r: + self.vy *= -1.0 + self.y_m = np.clip(self.y_m, -half_r, half_r) + + +# ==================== FUSED BODY (ZUSAMMENGESETZTER KÖRPER) ==================== +class FusedBody: + """Repräsentiert einen dynamischen Körper (1 oder mehrere verschmolzene Punkte).""" + def __init__(self, points, base_ratio=1.0): + self.points = points + self.base_ratio = base_ratio + + self.closest_point = None + self.min_dist_m = 999.0 + self.target_freq = 220.0 + self.target_itd_samples = 0.0 + self.target_gain_l = 0.0 + self.target_gain_r = 0.0 + + # Interne Audio-States für stufenlose Übergänge + self.current_freq = 220.0 + self.current_itd = 0.0 + self.phase_1 = random.uniform(0, 2 * np.pi) + self.phase_2 = random.uniform(0, 2 * np.pi) + + def compute_spatial_params(self): + """Ermittelt den nächsten Punkt zum Nutzer und berechnet Frequenz sowie Azimut.""" + if not self.points: + return + + self.closest_point = min( + self.points, + key=lambda p: np.sqrt(p.x_m**2 + p.y_m**2) + ) + + self.min_dist_m = np.sqrt(self.closest_point.x_m**2 + self.closest_point.y_m**2) + clamped_dist = min(self.min_dist_m, MAX_MAPPED_DIST) + norm_dist = clamped_dist / MAX_MAPPED_DIST + + # Frequenz-Mapping (nah = hoch, fern = tief) + base_f_near = 880.0 * self.base_ratio + base_f_far = 220.0 * self.base_ratio + self.target_freq = base_f_near * ((base_f_far / base_f_near) ** norm_dist) + + # Azimut bezogen auf den nächsten Punkt + azimuth = np.arctan2(self.closest_point.x_m, self.closest_point.y_m) + + # ITD Berechnung + itd_sec = (HEAD_RADIUS_M / C_SOUND) * (np.sin(azimuth) + azimuth) + self.target_itd_samples = itd_sec * SAMPLE_RATE + self.azimuth = azimuth + + def apply_relative_volume(self, global_min_dist): + """ + Berechnet die Lautstärke relativ zum nahesten Objekt im gesamten Raum. + - Das nächste Objekt (Delta = 0m) erhält 100% der Basis-Lautstärke. + - Weiter entfernte Objekte werden proportional zur Distanzdifferenz leiser. + """ + pan = np.sin(self.azimuth) + + # Basis-Lautstärke des nahesten Objekts + base_master_vol = 0.25 + + # Relativer Dämpfungsfaktor basierend auf der Differenz zum nahesten Objekt + dist_delta = self.min_dist_m - global_min_dist + + # Abfall-Intensität: Bei 5m Zusatzabstand sinkt die Lautstärke auf ~15% + rel_attenuation = np.exp(-0.4 * max(0.0, dist_delta)) + + effective_vol = base_master_vol * rel_attenuation + + self.target_gain_l = np.clip(0.5 * (1.0 - pan), 0.02, 1.0) * effective_vol + self.target_gain_r = np.clip(0.5 * (1.0 + pan), 0.02, 1.0) * effective_vol + + +# ==================== GLOBALE VARIABLEN & CLUSTER-LOGIK ==================== +points_list = [] +fused_bodies = [] + + +def update_clusters(): + """Identifiziert Punkte-Cluster, fusioniert sie und berechnet relative Lautstärken.""" + global fused_bodies + + n = len(points_list) + if n == 0: + fused_bodies = [] + return + + # 1. Graph-Cluster-Erkennung + visited = [False] * n + clusters = [] + + for i in range(n): + if not visited[i]: + cluster = [] + queue = [i] + visited[i] = True + + while queue: + curr = queue.pop(0) + cluster.append(points_list[curr]) + + for neighbor in range(n): + if not visited[neighbor]: + dx = points_list[curr].x_m - points_list[neighbor].x_m + dy = points_list[curr].y_m - points_list[neighbor].y_m + dist = np.sqrt(dx**2 + dy**2) + + if dist <= CLUSTER_THRESHOLD_M: + visited[neighbor] = True + queue.append(neighbor) + + clusters.append(cluster) + + # 2. FusedBody Instanzen erstellen / Parameter berechnen + new_fused_bodies = [] + for idx, cluster_points in enumerate(clusters): + ratio = CHORD_RATIOS[idx % len(CHORD_RATIOS)] + body = FusedBody(cluster_points, base_ratio=ratio) + body.compute_spatial_params() + new_fused_bodies.append(body) + + # 3. Globale euklidische Minimaldistanz ermitteln + global_min_dist = min(b.min_dist_m for b in new_fused_bodies) + + # 4. Lautstärke jedes Objekts relativ zum nahesten Objekt anpassen + for body in new_fused_bodies: + body.apply_relative_volume(global_min_dist) + + fused_bodies = new_fused_bodies + + +# ==================== AUDIO CALLBACK ==================== +def audio_callback(outdata, frames, time_info, status): + if status: + print(status, file=sys.stderr) + + outdata.fill(0.0) + if not fused_bodies: + return + + t_indices = np.arange(frames) + + for body in list(fused_bodies): + freq_vec = np.linspace(body.current_freq, body.target_freq, frames) + itd_vec = np.linspace(body.current_itd, body.target_itd_samples, frames) + + body.current_freq = body.target_freq + body.current_itd = body.target_itd_samples + + dphase_1 = 2 * np.pi * freq_vec / SAMPLE_RATE + dphase_2 = 2 * np.pi * (freq_vec * 1.498) / SAMPLE_RATE + + phases_1 = body.phase_1 + np.cumsum(dphase_1) + phases_2 = body.phase_2 + np.cumsum(dphase_2) + + body.phase_1 = phases_1[-1] % (2 * np.pi) + body.phase_2 = phases_2[-1] % (2 * np.pi) + + wave_1 = np.sin(phases_1) + wave_2 = 0.25 * np.sin(phases_2) + raw_signal = 0.18 * (wave_1 + wave_2) + + idx_l = t_indices + (itd_vec / 2.0) + idx_r = t_indices - (itd_vec / 2.0) + + sig_l = np.interp(idx_l, t_indices, raw_signal) * body.target_gain_l + sig_r = np.interp(idx_r, t_indices, raw_signal) * body.target_gain_r + + outdata[:, 0] += sig_l + outdata[:, 1] += sig_r + + +# ==================== HAUPTPROGRAMM ==================== +def main(): + pygame.init() + screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE)) + pygame.display.set_caption("Simulation 02: Dynamic Fusion & Relative Volume Attenuation") + clock = pygame.time.Clock() + + stream = sd.OutputStream( + channels=2, + samplerate=SAMPLE_RATE, + blocksize=BLOCK_SIZE, + callback=audio_callback + ) + + with stream: + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + running = False + elif event.key == pygame.K_c: + points_list.clear() + + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + m_px, m_py = event.pos + x_m = (m_px - WINDOW_SIZE / 2.0) / PIXELS_PER_METER + y_m = (WINDOW_SIZE / 2.0 - m_py) / PIXELS_PER_METER + points_list.append(MovingPoint(x_m, y_m)) + + # 1. Physik aktualisieren + for pt in points_list: + pt.update_physics() + + # 2. Cluster und relative Lautstärken berechnen + update_clusters() + + # --- RENDERING --- + screen.fill((15, 18, 25)) + center_px = WINDOW_SIZE // 2 + + # Raster & Abstandskreise + pygame.draw.line(screen, (35, 40, 55), (0, center_px), (WINDOW_SIZE, center_px), 1) + pygame.draw.line(screen, (35, 40, 55), (center_px, 0), (center_px, WINDOW_SIZE), 1) + + for r_m in range(2, 11, 2): + r_px = int(r_m * PIXELS_PER_METER) + pygame.draw.circle(screen, (30, 35, 50), (center_px, center_px), r_px, 1) + + # Nutzer-Kopf + head_radius_px = int(HEAD_RADIUS_M * 3 * PIXELS_PER_METER) + pygame.draw.circle(screen, (180, 190, 200), (center_px, center_px), head_radius_px) + pygame.draw.polygon(screen, (230, 90, 90), [ + (center_px - 8, center_px - head_radius_px), + (center_px + 8, center_px - head_radius_px), + (center_px, center_px - head_radius_px - 12) + ]) + + # Rote Verbindungsstriche und Richtungsvektoren + for body in fused_bodies: + pts = body.points + for i in range(len(pts)): + for j in range(i + 1, len(pts)): + dx = pts[i].x_m - pts[j].x_m + dy = pts[i].y_m - pts[j].y_m + if np.sqrt(dx**2 + dy**2) <= CLUSTER_THRESHOLD_M: + px1 = int(center_px + pts[i].x_m * PIXELS_PER_METER) + py1 = int(center_px - pts[i].y_m * PIXELS_PER_METER) + px2 = int(center_px + pts[j].x_m * PIXELS_PER_METER) + py2 = int(center_px - pts[j].y_m * PIXELS_PER_METER) + pygame.draw.line(screen, (240, 60, 60), (px1, py1), (px2, py2), 3) + + if body.closest_point: + cp_px = int(center_px + body.closest_point.x_m * PIXELS_PER_METER) + cp_py = int(center_px - body.closest_point.y_m * PIXELS_PER_METER) + # Hauptobjekt erhält hellen Vektor, entferntere Objekte gedämpfte Vektoren + pygame.draw.line(screen, (80, 220, 160, 80), (center_px, center_px), (cp_px, cp_py), 1) + + # Punkte zeichnen + for pt in points_list: + px = int(center_px + pt.x_m * PIXELS_PER_METER) + py = int(center_px - pt.y_m * PIXELS_PER_METER) + pygame.draw.circle(screen, (100, 255, 180), (px, py), 6) + + # HUD + font = pygame.font.SysFont("Consolas", 15) + hud_info = [ + f"Punkte gesamt : {len(points_list)}", + f"Aktive Körper : {len(fused_bodies)} (Relative Dämpfung aktiv)", + "[ Links-Klick ] : Punkt droppen", + "[ Taste 'C' ] : Alle Punkte löschen", + "[ ESC ] : Beenden" + ] + + for idx, text in enumerate(hud_info): + txt_surface = font.render(text, True, (200, 200, 210)) + screen.blit(txt_surface, (15, 15 + idx * 20)) + + pygame.display.flip() + clock.tick(60) + + pygame.quit() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/04_training_game.py b/src/04_training_game.py new file mode 100644 index 0000000..52071a7 --- /dev/null +++ b/src/04_training_game.py @@ -0,0 +1,318 @@ +import sys +import random +import time +import numpy as np +import pygame +import sounddevice as sd + +# ==================== KONFIGURATION & PARAMETER ==================== +ROOM_SIZE_M = 20.0 +WINDOW_SIZE = 800 +PIXELS_PER_METER = WINDOW_SIZE / ROOM_SIZE_M + +SAMPLE_RATE = 44100 +BLOCK_SIZE = 1024 +C_SOUND = 343.0 +HEAD_RADIUS_M = 0.0875 +MAX_MAPPED_DIST = 10.0 + +FREQ_MIN_DIST = 880.0 +FREQ_MAX_DIST = 220.0 + +REVEAL_DURATION = 2.0 # Sekunden Anzeige der Einzelauswertung +TOTAL_ITERATIONS = 5 # Runden bis zur Metrik-Auswertung + + +# ==================== TRAININGS-OBJEKT ==================== +class TargetObject: + """Repräsentiert das unsichtbare Ziel-Objekt im Trainingsmodus.""" + def __init__(self): + self.reset() + + def reset(self): + self.x_m = random.uniform(-8.0, 8.0) + self.y_m = random.uniform(-8.0, 8.0) + + self.vx = random.uniform(-0.02, 0.02) + self.vy = random.uniform(-0.02, 0.02) + + self.target_freq = 220.0 + self.target_itd_samples = 0.0 + self.target_gain_l = 0.2 + self.target_gain_r = 0.2 + + self.current_freq = 220.0 + self.current_itd = 0.0 + self.phase_1 = 0.0 + self.phase_2 = 0.0 + + def update_physics(self): + """Bewegt das Objekt autonom im Raum.""" + self.x_m += self.vx + self.y_m += self.vy + + self.vx += random.uniform(-0.001, 0.001) + self.vy += random.uniform(-0.001, 0.001) + + speed = np.sqrt(self.vx**2 + self.vy**2) + if speed > 0.035: + self.vx = (self.vx / speed) * 0.035 + self.vy = (self.vy / speed) * 0.035 + + half_r = ROOM_SIZE_M / 2.0 - 0.5 + if abs(self.x_m) > half_r: + self.vx *= -1.0 + self.x_m = np.clip(self.x_m, -half_r, half_r) + if abs(self.y_m) > half_r: + self.vy *= -1.0 + self.y_m = np.clip(self.y_m, -half_r, half_r) + + def update_audio_params(self): + """Berechnet Frequenz, ITD und ILD basierend auf der aktuellen Position.""" + dist_m = np.sqrt(self.x_m**2 + self.y_m**2) + clamped_dist = min(dist_m, MAX_MAPPED_DIST) + norm_dist = clamped_dist / MAX_MAPPED_DIST + + self.target_freq = FREQ_MIN_DIST * ((FREQ_MAX_DIST / FREQ_MIN_DIST) ** norm_dist) + + azimuth = np.arctan2(self.x_m, self.y_m) + itd_sec = (HEAD_RADIUS_M / C_SOUND) * (np.sin(azimuth) + azimuth) + self.target_itd_samples = itd_sec * SAMPLE_RATE + + pan = np.sin(azimuth) + master_vol = 0.25 + self.target_gain_l = np.clip(0.5 * (1.0 - pan), 0.05, 1.0) * master_vol + self.target_gain_r = np.clip(0.5 * (1.0 + pan), 0.05, 1.0) * master_vol + + +target_obj = TargetObject() + + +# ==================== AUDIO CALLBACK ==================== +def audio_callback(outdata, frames, time_info, status): + if status: + print(status, file=sys.stderr) + + t_indices = np.arange(frames) + + freq_vec = np.linspace(target_obj.current_freq, target_obj.target_freq, frames) + itd_vec = np.linspace(target_obj.current_itd, target_obj.target_itd_samples, frames) + + target_obj.current_freq = target_obj.target_freq + target_obj.current_itd = target_obj.target_itd_samples + + dphase_1 = 2 * np.pi * freq_vec / SAMPLE_RATE + dphase_2 = 2 * np.pi * (freq_vec * 1.498) / SAMPLE_RATE + + phases_1 = target_obj.phase_1 + np.cumsum(dphase_1) + phases_2 = target_obj.phase_2 + np.cumsum(dphase_2) + + target_obj.phase_1 = phases_1[-1] % (2 * np.pi) + target_obj.phase_2 = phases_2[-1] % (2 * np.pi) + + wave_1 = np.sin(phases_1) + wave_2 = 0.25 * np.sin(phases_2) + raw_signal = 0.2 * (wave_1 + wave_2) + + idx_l = t_indices + (itd_vec / 2.0) + idx_r = t_indices - (itd_vec / 2.0) + + outdata[:, 0] = np.interp(idx_l, t_indices, raw_signal) * target_obj.target_gain_l + outdata[:, 1] = np.interp(idx_r, t_indices, raw_signal) * target_obj.target_gain_r + + +# ==================== HAUPTPROGRAMM ==================== +def main(): + pygame.init() + screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE)) + pygame.display.set_caption("Simulation 03: Training Mode with Metrics Evaluation") + clock = pygame.time.Clock() + + stream = sd.OutputStream( + channels=2, + samplerate=SAMPLE_RATE, + blocksize=BLOCK_SIZE, + callback=audio_callback + ) + + # Versuchs-Datenstrukturen + trials_data = [] # Liste von Dicts mit Daten jeder Runde + iteration = 0 + round_start_time = time.time() + + click_result = None + reveal_start_time = 0.0 + is_revealed = False + show_summary_screen = False + + with stream: + running = True + while running: + current_time = time.time() + + # Timer für Übergang zwischen Runden + if is_revealed and (current_time - reveal_start_time > REVEAL_DURATION): + is_revealed = False + click_result = None + + if iteration >= TOTAL_ITERATIONS: + show_summary_screen = True + else: + target_obj.reset() + round_start_time = time.time() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + running = False + elif event.key == pygame.K_SPACE and show_summary_screen: + # Testreihe zurücksetzen für nächste 5 Runden + trials_data.clear() + iteration = 0 + show_summary_screen = False + target_obj.reset() + round_start_time = time.time() + + elif event.type == pygame.MOUSEBUTTONDOWN and not is_revealed and not show_summary_screen: + if event.button == 1: + decision_time = current_time - round_start_time + + m_px, m_py = event.pos + click_x_m = (m_px - WINDOW_SIZE / 2.0) / PIXELS_PER_METER + click_y_m = (WINDOW_SIZE / 2.0 - m_py) / PIXELS_PER_METER + + target_x_m = target_obj.x_m + target_y_m = target_obj.y_m + + err_x = abs(click_x_m - target_x_m) + err_y = abs(click_y_m - target_y_m) + dist_error = np.sqrt((click_x_m - target_x_m)**2 + (click_y_m - target_y_m)**2) + + iteration += 1 + + trial_info = { + "round": iteration, + "click_x": click_x_m, + "click_y": click_y_m, + "target_x": target_x_m, + "target_y": target_y_m, + "err_x": err_x, + "err_y": err_y, + "total_err": dist_error, + "decision_time": decision_time + } + trials_data.append(trial_info) + + click_result = trial_info + is_revealed = True + reveal_start_time = current_time + + # Objekt nur bewegen, wenn active Phase + if not is_revealed and not show_summary_screen: + target_obj.update_physics() + target_obj.update_audio_params() + + # --- RENDERING --- + screen.fill((15, 18, 25)) + center_px = WINDOW_SIZE // 2 + + # Raster & Abstandskreise + pygame.draw.line(screen, (35, 40, 55), (0, center_px), (WINDOW_SIZE, center_px), 1) + pygame.draw.line(screen, (35, 40, 55), (center_px, 0), (center_px, WINDOW_SIZE), 1) + + for r_m in range(2, 11, 2): + r_px = int(r_m * PIXELS_PER_METER) + pygame.draw.circle(screen, (30, 35, 50), (center_px, center_px), r_px, 1) + + # Nutzer-Kopf + head_radius_px = int(HEAD_RADIUS_M * 3 * PIXELS_PER_METER) + pygame.draw.circle(screen, (180, 190, 200), (center_px, center_px), head_radius_px) + pygame.draw.polygon(screen, (230, 90, 90), [ + (center_px - 8, center_px - head_radius_px), + (center_px + 8, center_px - head_radius_px), + (center_px, center_px - head_radius_px - 12) + ]) + + # 1. Einzel-Runden Reveal Visualisierung + if is_revealed and click_result is not None: + t_px = int(center_px + click_result["target_x"] * PIXELS_PER_METER) + t_py = int(center_px - click_result["target_y"] * PIXELS_PER_METER) + c_px = int(center_px + click_result["click_x"] * PIXELS_PER_METER) + c_py = int(center_px - click_result["click_y"] * PIXELS_PER_METER) + + pygame.draw.line(screen, (255, 200, 80), (c_px, c_py), (t_px, t_py), 2) + pygame.draw.circle(screen, (80, 220, 160), (t_px, t_py), 10) + + cross_size = 8 + pygame.draw.line(screen, (240, 70, 70), (c_px - cross_size, c_py - cross_size), (c_px + cross_size, c_py + cross_size), 3) + pygame.draw.line(screen, (240, 70, 70), (c_px - cross_size, c_py + cross_size), (c_px + cross_size, c_py - cross_size), 3) + + # 2. FINALES ERGEBNIS-PANEL (NACH 5 ITERATIONEN) + if show_summary_screen: + # Transparenter Overlay-Hintergrund + overlay = pygame.Surface((WINDOW_SIZE, WINDOW_SIZE)) + overlay.set_alpha(220) + overlay.fill((10, 12, 18)) + screen.blit(overlay, (0, 0)) + + # Metriken berechnen + mean_err_x = np.mean([t["err_x"] for t in trials_data]) + mean_err_y = np.mean([t["err_y"] for t in trials_data]) + mean_total_err = np.mean([t["total_err"] for t in trials_data]) + mean_time = np.mean([t["decision_time"] for t in trials_data]) + + font_title = pygame.font.SysFont("Consolas", 22, bold=True) + font_body = pygame.font.SysFont("Consolas", 16) + font_highlight = pygame.font.SysFont("Consolas", 17, bold=True) + + title_surf = font_title.render("--- EVALUATION ERGEBNISSE (5 ITERATIONEN) ---", True, (80, 220, 160)) + screen.blit(title_surf, (100, 120)) + + metrics_display = [ + (f"Mean Fehler X-Achse : {mean_err_x:.3f} m", (220, 220, 230)), + (f"Mean Fehler Y-Achse : {mean_err_y:.3f} m", (220, 220, 230)), + (f"Mean Euklid. Distanz : {mean_total_err:.3f} m", (255, 200, 80)), + (f"Gebrauchte Zeit / Item : {mean_time:.2f} Sekunden", (100, 200, 255)), + ] + + for idx, (text, color) in enumerate(metrics_display): + txt_surf = font_body.render(text, True, color) + screen.blit(txt_surf, (120, 180 + idx * 30)) + + # Einzelübersicht der 5 Runden + y_offset = 330 + header_surf = font_highlight.render("Detailübersicht der Runden:", True, (180, 190, 200)) + screen.blit(header_surf, (120, y_offset)) + + for t in trials_data: + y_offset += 25 + row_txt = f"Runde {t['round']}: Fehler = {t['total_err']:.2f} m (X: {t['err_x']:.2f}m, Y: {t['err_y']:.2f}m) | Zeit: {t['decision_time']:.2f}s" + row_surf = font_body.render(row_txt, True, (160, 170, 185)) + screen.blit(row_surf, (120, y_offset)) + + footer_surf = font_highlight.render("[ PRESS SPACE ] Nächste Testreihe starten | [ ESC ] Beenden", True, (80, 220, 160)) + screen.blit(footer_surf, (100, 680)) + + else: + # HUD im aktiven Modus + font = pygame.font.SysFont("Consolas", 15) + hud_info = [ + f"--- TRAININGSMODUS (Runde {iteration + 1} / {TOTAL_ITERATIONS}) ---", + "Klicke auf die vermutete Position des Objekts!", + f"Verstreichende Zeit: {current_time - round_start_time:.1f} s" if not is_revealed else "Auflösung läuft..." + ] + + for idx, text in enumerate(hud_info): + txt_surface = font.render(text, True, (200, 200, 210)) + screen.blit(txt_surface, (15, 15 + idx * 20)) + + pygame.display.flip() + clock.tick(60) + + pygame.quit() + + +if __name__ == "__main__": + main() \ No newline at end of file