test1 / watch_model.py
ahm3texe's picture
Upload 28 files
f083c5c verified
#!/usr/bin/env python3
"""
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
# Add pydino directory to sys.path
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
# Constants (must match training)
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 # Track last 10K milestone for silent mode
# Night mode variables (matching original Runner)
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)
# Visualization toggles
self.viz_bezier = True # B - Use Bezier curves
self.viz_width = True # W - Edge width proportional to weights
self.viz_opacity = True # O - Edge opacity proportional to weights
self.viz_color = True # C - Edge color by weight sign (blue/red vs gray)
# Replace default trex with our NeuroTrex
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 # For compatibility with base Runner
def _get_inputs(self):
"""Get inputs for the brain (same as training)."""
dino = self.dino
speed = self.current_speed / self.config.maxSpeed
# Dino state
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
# Obstacles
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 # Fixed 60 FPS physics
self.time_ms = now
# Clear screen (skip in silent mode)
if not self.silent:
self.screen.fill((247, 247, 247))
# Update game state
if self.playing:
self.running_time += delta
has_obstacles = self.running_time > self.config.clearTime
# Night mode logic (from original Runner) - always update timer, even in turbo
show_night_mode = self.inverted
if self.playing:
actual_score = int(self.distance_ran * 0.025)
# Timer-based fade cycle
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:
# Trigger at each invertDistance milestone
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
# Draw horizon FIRST (includes moon, stars, clouds, ground)
self.horizon.update(delta, self.current_speed, has_obstacles, show_night_mode)
# AI Decision and Dino update (draws dino ON TOP of horizon)
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) # This draws dino
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))
# Collision detection
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)
# Silent mode: Print every 10K
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
# Draw game over panel if crashed (skip in silent mode)
if not self.silent and self.crashed and self.game_over_panel:
self.game_over_panel.draw(False, self.dino)
# Apply night overlay (skip in silent mode)
if not self.silent:
fade_factor = self._get_invert_fade_factor()
if fade_factor > 0:
self._apply_night_overlay(fade_factor)
# Draw stats overlay (skip in silent mode)
# Blit game surface to main screen (centered)
if not self.silent and hasattr(self, 'main_screen'):
# Clear main screen
self.main_screen.fill((247, 247, 247))
# Blit game surface centered
offset = getattr(self, 'game_offset', 0)
self.main_screen.blit(self.screen, (offset, 0))
# Draw stats and brain on main screen
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:
# Get pixels as array (only for game area, not brain viz)
game_rect = pygame.Rect(0, 0, self.screen.get_width(), 150)
game_surface = self.screen.subsurface(game_rect)
# Convert to array, invert, then blend
pixels = pygame.surfarray.pixels3d(game_surface)
inverted = 255 - pixels
# Blend between original and inverted based on fade_factor
blended = pixels * (1.0 - fade_factor) + inverted * fade_factor
np.copyto(pixels, blended.astype(np.uint8))
del pixels # Release surface lock
except Exception:
# Fallback to dark overlay if pixel manipulation fails
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 # Reset 10K tracker
self.current_speed = self.config.speed
self.running_time = 0
# Reset night mode
self.inverted = False
self.invert_timer = 0
self.invert_trigger = False
self.horizon.reset()
if not self.silent:
self.distance_meter.reset()
# Reset dino
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
# Background - white
surf = pygame.Surface((w, h))
surf.fill((255, 255, 255))
target_screen.blit(surf, (0, start_y))
# Layout - use screen width for positioning
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 output nodes vertically (3 nodes with 60px spacing)
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
# Control points for smooth curve
mid_x = (x1 + x2) // 2
ctrl1 = (mid_x, y1)
ctrl2 = (mid_x, y2)
# Generate curve points
points = []
for t in range(0, 21):
t = t / 20.0
# Cubic Bezier formula
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)
# Draw weights with curves or lines (only strong connections)
def draw_edge(start, end, weight):
# Base color based on viz_color toggle
if self.viz_color:
# Colored mode: blue = positive, red = negative
if weight < 0:
base_color = (255, 0, 0) # Red for negative
else:
base_color = (0, 0, 255) # Blue for positive
else:
# Gray mode
base_color = (80, 80, 80)
# Apply opacity if enabled - NN-SVG style (linear 0-1, weak weights invisible)
if self.viz_opacity:
# Linear scale like NN-SVG: domain([0, 1]).range([0, 1])
w_norm = min(1.0, abs(weight))
if w_norm < 0.05:
return # Skip drawing very weak connections
# Blend from white background (255,255,255) to base_color based on weight
# weak = white (invisible on white bg), strong = base_color
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
# Apply width if enabled - FCNN style (weak = thin/invisible)
if self.viz_width:
# Linear scale: weight 0 -> width 0, weight 1 -> width 3
width = int(abs(weight) * 3)
if width < 1:
return # Skip drawing very thin connections
else:
width = 1
# Draw Bezier or straight line
if self.viz_bezier:
draw_bezier(start, end, color, width)
else:
pygame.draw.line(target_screen, color, start, end, width)
# Threshold: show all when opacity OFF, filter weak when ON
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)
# Draw input nodes
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))
# Draw hidden nodes
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)
# Draw output nodes
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()
# Load brain
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)
# Silent mode setup
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")
# Initialize pygame
pygame.init()
if not args.silent:
pygame.display.set_caption("NeuroDino - Model Watcher")
# Game area stays fixed at 600x150, neural network area expanded
dims = Dimensions(width=600, height=150)
screen = pygame.display.set_mode((900, 850)) # Wider and taller for NN
# Create separate surface for game at original size
game_surface = pygame.Surface((dims.width, dims.height))
game_offset = (900 - 600) // 2 # Center horizontally
clock = pygame.time.Clock()
# Create watcher - pass game_surface as the drawing target for the game
watcher = ModelWatcher(game_surface, dims, brain, silent=args.silent)
watcher.main_screen = screen # Store main screen for NN drawing
watcher.game_offset = game_offset # Store offset for centering
watcher.start()
# FPS modes: 0=60fps, 1=unlimited, 2=turbo
speed_mode = 0
if args.fast:
speed_mode = 1
if args.silent:
speed_mode = 2
turbo_mode = False # Runtime turbo toggle (no drawing)
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:
# FPS control based on mode
if speed_mode == 0:
clock.tick(60)
else:
clock.tick() # Unlimited
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:
# Cycle through 3 modes
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()
# Auto-restart on crash
if watcher.crashed:
if not args.silent and not turbo_mode:
pygame.time.wait(500)
watcher.restart_game()
# Logging for Turbo/Silent/Headless Modes
if args.silent or turbo_mode:
current_time = pygame.time.get_ticks()
if current_time - last_log_time > 1000: # 10 seconds
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
# Update title (only in display mode)
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()