|
|
|
|
|
""" |
|
|
NeuroDino - Model Watcher (Inference Mode) |
|
|
|
|
|
Watch your trained brain play the game without training. |
|
|
Press 'S' to toggle between 60 FPS and Unlimited FPS. |
|
|
Press 'R' to restart the game. |
|
|
Press 'Q' or ESC to quit. |
|
|
|
|
|
Usage: |
|
|
python watch_model.py # Load best_brain.pkl |
|
|
python watch_model.py --brain path.pkl # Load specific brain file |
|
|
python watch_model.py --fast # Start in unlimited FPS mode |
|
|
python watch_model.py --silent # No display, just run and print scores |
|
|
""" |
|
|
|
|
|
import os |
|
|
import sys |
|
|
import argparse |
|
|
import pickle |
|
|
import pygame |
|
|
|
|
|
|
|
|
pydino_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pydino") |
|
|
sys.path.append(pydino_path) |
|
|
|
|
|
from dimensions import Dimensions |
|
|
from pydino.runner import Runner, Config |
|
|
from pydino.trex import Trex, Status as TrexStatus |
|
|
from neurodino.neuro_trex import NeuroTrex |
|
|
from neurodino.brain import Brain |
|
|
import numpy as np |
|
|
import math |
|
|
|
|
|
|
|
|
GAME_HEIGHT = 150 |
|
|
MAX_OBSTACLE_WIDTH = 75 |
|
|
MAX_TTI_FRAMES = 50.0 |
|
|
DUCK_THRESHOLD_Y = 75 |
|
|
|
|
|
|
|
|
class ModelWatcher(Runner): |
|
|
""" |
|
|
Simplified runner for watching a single trained brain play. |
|
|
No training, no population - just inference. |
|
|
""" |
|
|
|
|
|
def __init__(self, screen, dimensions, brain: Brain, target_fps=60, silent=False): |
|
|
super().__init__(screen, dimensions, use_audio=not silent) |
|
|
|
|
|
self.brain = brain |
|
|
self.target_fps = target_fps |
|
|
self.score = 0 |
|
|
self.high_score = 0 |
|
|
self.games_played = 0 |
|
|
self.silent = silent |
|
|
self.last_10k = 0 |
|
|
|
|
|
|
|
|
self.inverted = False |
|
|
self.invert_timer = 0 |
|
|
self.invert_trigger = False |
|
|
self.NIGHT_ALPHA_MAX = 180 |
|
|
self.VISUAL_INVERT_MS = 1000 |
|
|
self._night_overlay = pygame.Surface((dimensions.width, dimensions.height), pygame.SRCALPHA) |
|
|
|
|
|
|
|
|
self.viz_bezier = True |
|
|
self.viz_width = True |
|
|
self.viz_opacity = True |
|
|
self.viz_color = True |
|
|
|
|
|
|
|
|
self._setup_neuro_trex() |
|
|
|
|
|
def _setup_neuro_trex(self): |
|
|
"""Setup a single NeuroTrex with the loaded brain.""" |
|
|
self.dino = NeuroTrex(self.screen, self.sprite_def["tRex"], self) |
|
|
self.dino.brain = self.brain |
|
|
self.dino.visible = True |
|
|
self.trex = self.dino |
|
|
|
|
|
def _get_inputs(self): |
|
|
"""Get inputs for the brain (same as training).""" |
|
|
dino = self.dino |
|
|
speed = self.current_speed / self.config.maxSpeed |
|
|
|
|
|
|
|
|
ground_y = dino.groundYPos |
|
|
max_jump = dino.config.maxJumpHeight |
|
|
|
|
|
dino_y_normalized = 0.0 |
|
|
if dino.jumping: |
|
|
height_above_ground = ground_y - dino.yPos |
|
|
dino_y_normalized = min(1.0, max(0.0, height_above_ground / max_jump)) |
|
|
|
|
|
dino_velocity = 0.0 |
|
|
if dino.jumping: |
|
|
dino_velocity = max(-1.0, min(1.0, dino.jumpVelocity / 10.0)) |
|
|
|
|
|
is_airborne = 1.0 if dino.jumping else 0.0 |
|
|
is_ducking = 1.0 if dino.ducking else 0.0 |
|
|
|
|
|
|
|
|
obs1_dist = 0.0 |
|
|
obs1_action = 0.0 |
|
|
obs1_w = 0.0 |
|
|
obs2_dist = 0.0 |
|
|
obs2_action = 0.0 |
|
|
obs2_w = 0.0 |
|
|
gap = 0.0 |
|
|
|
|
|
if self.horizon and self.horizon.obstacles: |
|
|
dino_front = dino.xPos |
|
|
future_obstacles = [o for o in self.horizon.obstacles if o.xPos > dino_front] |
|
|
future_obstacles.sort(key=lambda o: o.xPos) |
|
|
|
|
|
if len(future_obstacles) > 0: |
|
|
o1 = future_obstacles[0] |
|
|
dist1 = o1.xPos - dino.xPos |
|
|
tti1 = dist1 / max(1.0, self.current_speed) |
|
|
obs1_dist = 1.0 - min(1.0, tti1 / MAX_TTI_FRAMES) |
|
|
obs1_action = 1.0 if o1.yPos < DUCK_THRESHOLD_Y else 0.0 |
|
|
obs1_w = min(1.0, o1.width / MAX_OBSTACLE_WIDTH) |
|
|
|
|
|
if len(future_obstacles) > 1: |
|
|
o2 = future_obstacles[1] |
|
|
dist2 = o2.xPos - dino.xPos |
|
|
tti2 = dist2 / max(1.0, self.current_speed) |
|
|
obs2_dist = 1.0 - min(1.0, tti2 / MAX_TTI_FRAMES) |
|
|
obs2_action = 1.0 if o2.yPos < DUCK_THRESHOLD_Y else 0.0 |
|
|
obs2_w = min(1.0, o2.width / MAX_OBSTACLE_WIDTH) |
|
|
|
|
|
raw_gap = o2.xPos - (o1.xPos + o1.width) |
|
|
time_gap = raw_gap / max(1.0, self.current_speed) |
|
|
gap = 1.0 - min(1.0, time_gap / 15.0) |
|
|
|
|
|
return np.array([ |
|
|
obs1_dist, obs1_action, obs1_w, |
|
|
obs2_dist, obs2_action, obs2_w, |
|
|
speed, gap, |
|
|
dino_y_normalized, dino_velocity, |
|
|
is_airborne, is_ducking |
|
|
]) |
|
|
|
|
|
def update(self): |
|
|
"""Game loop with AI control.""" |
|
|
now = pygame.time.get_ticks() |
|
|
delta = 1000.0 / 60 |
|
|
self.time_ms = now |
|
|
|
|
|
|
|
|
if not self.silent: |
|
|
self.screen.fill((247, 247, 247)) |
|
|
|
|
|
|
|
|
if self.playing: |
|
|
self.running_time += delta |
|
|
|
|
|
has_obstacles = self.running_time > self.config.clearTime |
|
|
|
|
|
|
|
|
show_night_mode = self.inverted |
|
|
if self.playing: |
|
|
actual_score = int(self.distance_ran * 0.025) |
|
|
|
|
|
|
|
|
if self.invert_timer > self.config.invertFadeDuration: |
|
|
self.invert_timer = 0 |
|
|
self.invert_trigger = False |
|
|
self.inverted = not self.inverted |
|
|
elif self.invert_timer > 0: |
|
|
self.invert_timer += delta |
|
|
else: |
|
|
|
|
|
if actual_score > 0 and (actual_score % self.config.invertDistance == 0) and not self.invert_trigger: |
|
|
self.invert_timer += delta |
|
|
self.inverted = not self.inverted |
|
|
self.invert_trigger = True |
|
|
elif (actual_score % self.config.invertDistance) != 0: |
|
|
self.invert_trigger = False |
|
|
|
|
|
|
|
|
self.horizon.update(delta, self.current_speed, has_obstacles, show_night_mode) |
|
|
|
|
|
|
|
|
if self.playing and not self.crashed and self.dino.status != TrexStatus.CRASHED: |
|
|
inputs = self._get_inputs() |
|
|
outputs = self.dino.brain.predict(inputs) |
|
|
action = np.argmax(outputs) |
|
|
self.dino.act(action) |
|
|
self.dino.update(delta) |
|
|
|
|
|
if self.dino.jumping: |
|
|
self.dino.updateJump(delta) |
|
|
|
|
|
if self.playing and not self.silent: |
|
|
self.distance_meter.update(delta, math.ceil(self.distance_ran)) |
|
|
|
|
|
|
|
|
if self.playing and not self.crashed: |
|
|
if has_obstacles and self.horizon.obstacles: |
|
|
for obstacle in self.horizon.obstacles: |
|
|
if self._check_for_collision(obstacle, self.dino): |
|
|
self.dino.update(100, TrexStatus.CRASHED) |
|
|
self.crashed = True |
|
|
self._on_game_over() |
|
|
break |
|
|
|
|
|
if not self.crashed: |
|
|
self.distance_ran += self.current_speed * (delta / self.ms_per_frame) |
|
|
self.score = int(self.distance_ran * 0.025) |
|
|
|
|
|
|
|
|
if self.silent: |
|
|
current_10k = self.score // 10000 |
|
|
if current_10k > self.last_10k: |
|
|
self.last_10k = current_10k |
|
|
print(f"📈 Score: {self.score:,}") |
|
|
|
|
|
if self.current_speed < self.config.maxSpeed: |
|
|
self.current_speed += self.config.acceleration |
|
|
|
|
|
|
|
|
if not self.silent and self.crashed and self.game_over_panel: |
|
|
self.game_over_panel.draw(False, self.dino) |
|
|
|
|
|
|
|
|
if not self.silent: |
|
|
fade_factor = self._get_invert_fade_factor() |
|
|
if fade_factor > 0: |
|
|
self._apply_night_overlay(fade_factor) |
|
|
|
|
|
|
|
|
|
|
|
if not self.silent and hasattr(self, 'main_screen'): |
|
|
|
|
|
self.main_screen.fill((247, 247, 247)) |
|
|
|
|
|
offset = getattr(self, 'game_offset', 0) |
|
|
self.main_screen.blit(self.screen, (offset, 0)) |
|
|
|
|
|
self._draw_stats(self.main_screen, offset) |
|
|
self._draw_brain(self.main_screen) |
|
|
|
|
|
def _get_invert_fade_factor(self): |
|
|
"""Calculate fade factor for night mode transition (from original Runner).""" |
|
|
T = self.config.invertFadeDuration |
|
|
t = self.invert_timer |
|
|
vis = self.VISUAL_INVERT_MS |
|
|
|
|
|
if self.inverted: |
|
|
if t == 0: |
|
|
return 1.0 |
|
|
elif t < vis: |
|
|
return self._ease_in_out_cubic(t / vis) |
|
|
elif t <= (T - vis): |
|
|
return 1.0 |
|
|
else: |
|
|
return self._ease_in_out_cubic(1.0 - ((t - (T - vis)) / vis)) |
|
|
else: |
|
|
if t > 0 and t > (T - vis): |
|
|
return self._ease_in_out_cubic(1.0 - ((t - (T - vis)) / vis)) |
|
|
return 0.0 |
|
|
|
|
|
def _ease_in_out_cubic(self, t): |
|
|
"""CSS-like ease-in-out for smooth transitions.""" |
|
|
if t <= 0.0: |
|
|
return 0.0 |
|
|
if t >= 1.0: |
|
|
return 1.0 |
|
|
if t < 0.5: |
|
|
return 4.0 * t * t * t |
|
|
return 1.0 - ((-2.0 * t + 2.0) ** 3) / 2.0 |
|
|
|
|
|
def _apply_night_overlay(self, fade_factor): |
|
|
if fade_factor <= 0.0: |
|
|
return |
|
|
|
|
|
try: |
|
|
|
|
|
game_rect = pygame.Rect(0, 0, self.screen.get_width(), 150) |
|
|
game_surface = self.screen.subsurface(game_rect) |
|
|
|
|
|
|
|
|
pixels = pygame.surfarray.pixels3d(game_surface) |
|
|
inverted = 255 - pixels |
|
|
|
|
|
|
|
|
blended = pixels * (1.0 - fade_factor) + inverted * fade_factor |
|
|
np.copyto(pixels, blended.astype(np.uint8)) |
|
|
del pixels |
|
|
except Exception: |
|
|
|
|
|
alpha = int(self.NIGHT_ALPHA_MAX * fade_factor) |
|
|
if alpha > 0: |
|
|
self._night_overlay.fill((0, 0, 0, alpha)) |
|
|
self.screen.blit(self._night_overlay, (0, 0)) |
|
|
|
|
|
def _on_game_over(self): |
|
|
"""Handle game over.""" |
|
|
self.games_played += 1 |
|
|
if self.score > self.high_score: |
|
|
self.high_score = self.score |
|
|
|
|
|
if self.silent: |
|
|
print(f"💀 ÖLDÜ! Score: {self.score:,} | High: {self.high_score:,} | Game #{self.games_played}") |
|
|
else: |
|
|
print(f"Game {self.games_played}: Score {self.score} | High Score: {self.high_score}") |
|
|
|
|
|
def restart_game(self): |
|
|
"""Restart the game.""" |
|
|
self.crashed = False |
|
|
self.playing = True |
|
|
self.distance_ran = 0 |
|
|
self.score = 0 |
|
|
self.last_10k = 0 |
|
|
self.current_speed = self.config.speed |
|
|
self.running_time = 0 |
|
|
|
|
|
|
|
|
self.inverted = False |
|
|
self.invert_timer = 0 |
|
|
self.invert_trigger = False |
|
|
|
|
|
self.horizon.reset() |
|
|
if not self.silent: |
|
|
self.distance_meter.reset() |
|
|
|
|
|
|
|
|
self._setup_neuro_trex() |
|
|
self.dino.update(0, TrexStatus.RUNNING) |
|
|
|
|
|
def _draw_stats(self, target_screen, offset=0): |
|
|
"""Draw stats overlay - only speed.""" |
|
|
font = pygame.font.Font(None, 28) |
|
|
txt = font.render(f"Speed: {self.current_speed:.1f}", True, (80, 80, 80)) |
|
|
target_screen.blit(txt, (offset + 10, 10)) |
|
|
|
|
|
def _draw_brain(self, target_screen): |
|
|
"""Draw brain visualization.""" |
|
|
brain = self.brain |
|
|
if not hasattr(brain, "last_inputs"): |
|
|
return |
|
|
|
|
|
start_y = 150 |
|
|
w = target_screen.get_width() |
|
|
h = target_screen.get_height() - start_y |
|
|
|
|
|
|
|
|
surf = pygame.Surface((w, h)) |
|
|
surf.fill((255, 255, 255)) |
|
|
target_screen.blit(surf, (0, start_y)) |
|
|
|
|
|
|
|
|
layer_x = [80, w // 2, w - 80] |
|
|
input_y = np.linspace(start_y + 30, start_y + h - 30, brain.input_nodes) |
|
|
hidden_y = np.linspace(start_y + 20, start_y + h - 20, brain.hidden_nodes) |
|
|
|
|
|
|
|
|
center_y = start_y + h // 2 |
|
|
output_spacing = 80 |
|
|
output_y = [center_y - output_spacing, center_y, center_y + output_spacing] |
|
|
|
|
|
input_labels = ["O1 TTI", "O1 Act", "O1 W", "O2 TTI", "O2 Act", "O2 W", |
|
|
"Speed", "Gap", "DinoY", "DinoVel", "Air", "Duck"] |
|
|
output_labels = ["Jump", "Duck", "Run"] |
|
|
|
|
|
font = pygame.font.Font(None, 18) |
|
|
|
|
|
def get_color(val): |
|
|
v = max(0, min(1, abs(val))) |
|
|
return (int(v*255), int(v*255), int(v*255)) |
|
|
|
|
|
def draw_bezier(start, end, color, width=1): |
|
|
"""Draw a Bezier curve between two points.""" |
|
|
x1, y1 = start |
|
|
x2, y2 = end |
|
|
|
|
|
mid_x = (x1 + x2) // 2 |
|
|
ctrl1 = (mid_x, y1) |
|
|
ctrl2 = (mid_x, y2) |
|
|
|
|
|
|
|
|
points = [] |
|
|
for t in range(0, 21): |
|
|
t = t / 20.0 |
|
|
|
|
|
x = int((1-t)**3 * x1 + 3*(1-t)**2*t * ctrl1[0] + 3*(1-t)*t**2 * ctrl2[0] + t**3 * x2) |
|
|
y = int((1-t)**3 * y1 + 3*(1-t)**2*t * ctrl1[1] + 3*(1-t)*t**2 * ctrl2[1] + t**3 * y2) |
|
|
points.append((x, y)) |
|
|
|
|
|
if len(points) > 1: |
|
|
if width > 1: |
|
|
pygame.draw.lines(target_screen, color, False, points, width) |
|
|
else: |
|
|
pygame.draw.aalines(target_screen, color, False, points) |
|
|
|
|
|
|
|
|
def draw_edge(start, end, weight): |
|
|
|
|
|
if self.viz_color: |
|
|
|
|
|
if weight < 0: |
|
|
base_color = (255, 0, 0) |
|
|
else: |
|
|
base_color = (0, 0, 255) |
|
|
else: |
|
|
|
|
|
base_color = (80, 80, 80) |
|
|
|
|
|
|
|
|
if self.viz_opacity: |
|
|
|
|
|
w_norm = min(1.0, abs(weight)) |
|
|
if w_norm < 0.05: |
|
|
return |
|
|
|
|
|
|
|
|
color = (int(255 - (255 - base_color[0]) * w_norm), |
|
|
int(255 - (255 - base_color[1]) * w_norm), |
|
|
int(255 - (255 - base_color[2]) * w_norm)) |
|
|
else: |
|
|
color = base_color |
|
|
|
|
|
|
|
|
if self.viz_width: |
|
|
|
|
|
width = int(abs(weight) * 3) |
|
|
if width < 1: |
|
|
return |
|
|
else: |
|
|
width = 1 |
|
|
|
|
|
|
|
|
if self.viz_bezier: |
|
|
draw_bezier(start, end, color, width) |
|
|
else: |
|
|
pygame.draw.line(target_screen, color, start, end, width) |
|
|
|
|
|
|
|
|
threshold = 0.05 if self.viz_opacity else 0.001 |
|
|
|
|
|
for i in range(brain.input_nodes): |
|
|
for j in range(brain.hidden_nodes): |
|
|
weight = brain.weights_ih[j][i] |
|
|
if abs(weight) > threshold: |
|
|
draw_edge((layer_x[0], int(input_y[i])), |
|
|
(layer_x[1], int(hidden_y[j])), weight) |
|
|
|
|
|
for j in range(brain.hidden_nodes): |
|
|
for k in range(brain.output_nodes): |
|
|
weight = brain.weights_ho[k][j] |
|
|
if abs(weight) > threshold: |
|
|
draw_edge((layer_x[1], int(hidden_y[j])), |
|
|
(layer_x[2], int(output_y[k])), weight) |
|
|
|
|
|
|
|
|
for i, val in enumerate(brain.last_inputs): |
|
|
pos = (layer_x[0], int(input_y[i])) |
|
|
pygame.draw.circle(target_screen, (255, 255, 255), pos, 8) |
|
|
pygame.draw.circle(target_screen, (51, 51, 51), pos, 8, 1) |
|
|
lbl = font.render(f"{input_labels[i]}:{val:.2f}", True, (0, 0, 0)) |
|
|
target_screen.blit(lbl, (pos[0]-40, pos[1]-12)) |
|
|
|
|
|
|
|
|
for i, val in enumerate(brain.last_hidden): |
|
|
pos = (layer_x[1], int(hidden_y[i])) |
|
|
pygame.draw.circle(target_screen, (255, 255, 255), pos, 6) |
|
|
pygame.draw.circle(target_screen, (51, 51, 51), pos, 6, 1) |
|
|
|
|
|
|
|
|
max_idx = np.argmax(brain.last_outputs) |
|
|
for i, val in enumerate(brain.last_outputs): |
|
|
color = (0, 255, 0) if i == max_idx else (255, 255, 255) |
|
|
pos = (layer_x[2], int(output_y[i])) |
|
|
radius = 10 + int(val * 10) |
|
|
pygame.draw.circle(target_screen, color, pos, radius) |
|
|
pygame.draw.circle(target_screen, (51, 51, 51), pos, radius, 2) |
|
|
lbl = font.render(f"{output_labels[i]} ({val:.0%})", True, (0, 0, 0)) |
|
|
target_screen.blit(lbl, (pos[0]+20, pos[1]-8)) |
|
|
|
|
|
|
|
|
def main(): |
|
|
parser = argparse.ArgumentParser(description='Watch trained NeuroDino model') |
|
|
parser.add_argument('--brain', type=str, default='best_brain.pkl', |
|
|
help='Path to brain file (default: best_brain.pkl)') |
|
|
parser.add_argument('--fast', action='store_true', |
|
|
help='Start in unlimited FPS mode') |
|
|
parser.add_argument('--silent', action='store_true', |
|
|
help='No display, just run simulation and print scores') |
|
|
args = parser.parse_args() |
|
|
|
|
|
|
|
|
if not os.path.exists(args.brain): |
|
|
print(f"Error: Brain file not found: {args.brain}") |
|
|
print("Train a model first with: python main_train.py") |
|
|
sys.exit(1) |
|
|
|
|
|
try: |
|
|
with open(args.brain, "rb") as f: |
|
|
data = pickle.load(f) |
|
|
if isinstance(data, tuple): |
|
|
brain, score = data |
|
|
print(f"✅ Loaded brain from {args.brain}") |
|
|
print(f" Training score: {score:,}") |
|
|
else: |
|
|
brain = data |
|
|
print(f"✅ Loaded brain from {args.brain} (legacy format)") |
|
|
except Exception as e: |
|
|
print(f"Error loading brain: {e}") |
|
|
sys.exit(1) |
|
|
|
|
|
|
|
|
if args.silent: |
|
|
os.environ['SDL_VIDEODRIVER'] = 'dummy' |
|
|
os.environ['SDL_AUDIODRIVER'] = 'dummy' |
|
|
print("🔇 SILENT MODE - No display, maximum speed") |
|
|
print(" Press Ctrl+C to stop\n") |
|
|
|
|
|
|
|
|
pygame.init() |
|
|
|
|
|
if not args.silent: |
|
|
pygame.display.set_caption("NeuroDino - Model Watcher") |
|
|
|
|
|
|
|
|
dims = Dimensions(width=600, height=150) |
|
|
screen = pygame.display.set_mode((900, 850)) |
|
|
|
|
|
|
|
|
game_surface = pygame.Surface((dims.width, dims.height)) |
|
|
game_offset = (900 - 600) // 2 |
|
|
|
|
|
clock = pygame.time.Clock() |
|
|
|
|
|
|
|
|
watcher = ModelWatcher(game_surface, dims, brain, silent=args.silent) |
|
|
watcher.main_screen = screen |
|
|
watcher.game_offset = game_offset |
|
|
watcher.start() |
|
|
|
|
|
|
|
|
speed_mode = 0 |
|
|
if args.fast: |
|
|
speed_mode = 1 |
|
|
if args.silent: |
|
|
speed_mode = 2 |
|
|
|
|
|
turbo_mode = False |
|
|
|
|
|
if not args.silent: |
|
|
print("\n🎮 Controls:") |
|
|
print(" S - Toggle speed (60 FPS → Unlimited → Turbo)") |
|
|
print(" B - Toggle Bezier curves") |
|
|
print(" W - Toggle edge width proportional to weights") |
|
|
print(" O - Toggle edge opacity proportional to weights") |
|
|
print(" C - Toggle edge color (Blue/Red vs Gray)") |
|
|
print(" R - Restart game") |
|
|
print(" Q/ESC - Quit\n") |
|
|
|
|
|
|
|
|
running = True |
|
|
last_log_time = pygame.time.get_ticks() |
|
|
frame_count = 0 |
|
|
|
|
|
try: |
|
|
while running: |
|
|
|
|
|
if speed_mode == 0: |
|
|
clock.tick(60) |
|
|
else: |
|
|
clock.tick() |
|
|
|
|
|
frame_count += 1 |
|
|
|
|
|
for event in pygame.event.get(): |
|
|
if event.type == pygame.QUIT: |
|
|
running = False |
|
|
elif event.type == pygame.KEYDOWN and not args.silent: |
|
|
if event.key == pygame.K_q or event.key == pygame.K_ESCAPE: |
|
|
running = False |
|
|
elif event.key == pygame.K_s: |
|
|
|
|
|
speed_mode = (speed_mode + 1) % 3 |
|
|
turbo_mode = (speed_mode == 2) |
|
|
watcher.silent = turbo_mode |
|
|
|
|
|
mode_names = ["60 FPS", "UNLIMITED", "🚀 TURBO (no draw)"] |
|
|
print(f"Speed: {mode_names[speed_mode]}") |
|
|
elif event.key == pygame.K_r: |
|
|
watcher.restart_game() |
|
|
print("Game restarted!") |
|
|
elif event.key == pygame.K_b: |
|
|
watcher.viz_bezier = not watcher.viz_bezier |
|
|
print(f"Bezier curves: {'ON' if watcher.viz_bezier else 'OFF'}") |
|
|
elif event.key == pygame.K_w: |
|
|
watcher.viz_width = not watcher.viz_width |
|
|
print(f"Edge width: {'ON' if watcher.viz_width else 'OFF'}") |
|
|
elif event.key == pygame.K_o: |
|
|
watcher.viz_opacity = not watcher.viz_opacity |
|
|
print(f"Edge opacity: {'ON' if watcher.viz_opacity else 'OFF'}") |
|
|
elif event.key == pygame.K_c: |
|
|
watcher.viz_color = not watcher.viz_color |
|
|
print(f"Edge color: {'Blue/Red' if watcher.viz_color else 'Gray'}") |
|
|
|
|
|
watcher.update() |
|
|
|
|
|
if not args.silent and not turbo_mode: |
|
|
pygame.display.flip() |
|
|
|
|
|
|
|
|
if watcher.crashed: |
|
|
if not args.silent and not turbo_mode: |
|
|
pygame.time.wait(500) |
|
|
watcher.restart_game() |
|
|
|
|
|
|
|
|
if args.silent or turbo_mode: |
|
|
current_time = pygame.time.get_ticks() |
|
|
if current_time - last_log_time > 1000: |
|
|
elapsed_seconds = (current_time - last_log_time) / 1000.0 |
|
|
real_sps = int(frame_count / elapsed_seconds) |
|
|
print(f" [Watch] Score: {watcher.score:,} | High: {watcher.high_score:,} | SPS: {real_sps} (Sim/Sec)") |
|
|
|
|
|
last_log_time = current_time |
|
|
frame_count = 0 |
|
|
|
|
|
|
|
|
if not args.silent and not turbo_mode: |
|
|
mode_names = ["60", "MAX", "TURBO"] |
|
|
fps_val = clock.get_fps() |
|
|
if fps_val == float('inf') or fps_val > 99999: |
|
|
fps_text = f"MAX ({mode_names[speed_mode]})" |
|
|
else: |
|
|
fps_text = f"{int(fps_val)} ({mode_names[speed_mode]})" |
|
|
pygame.display.set_caption( |
|
|
f"NeuroDino | Score: {watcher.score:,} | High: {watcher.high_score:,} | FPS: {fps_text}" |
|
|
) |
|
|
except KeyboardInterrupt: |
|
|
print("\n\n⏹️ Stopped by user") |
|
|
|
|
|
pygame.quit() |
|
|
print(f"\n📊 Session Stats:") |
|
|
print(f" Games Played: {watcher.games_played}") |
|
|
print(f" High Score: {watcher.high_score}") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |
|
|
|