diff --git a/engines/headless_engine.py b/engines/headless_engine.py index 52957ae..cf5bef4 100644 --- a/engines/headless_engine.py +++ b/engines/headless_engine.py @@ -12,6 +12,11 @@ from output import MetricsCollector, EntityCollector, EvolutionCollector from output.formatters.json_formatter import JSONFormatter from output.formatters.csv_formatter import CSVFormatter from output.writers.file_writer import FileWriter +try: + from tqdm import tqdm + TQDM_AVAILABLE = True +except ImportError: + TQDM_AVAILABLE = False @dataclass @@ -56,6 +61,12 @@ class HeadlessSimulationEngine: 'evolution': [] } + # Progress tracking + self.files_written = 0 + self.last_progress_update = 0 + self.progress_update_interval = 1.0 # Update progress every second + self.progress_bar = None + # Setup signal handlers for graceful shutdown signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) @@ -88,6 +99,120 @@ class HeadlessSimulationEngine: return collectors + def _init_progress_bar(self): + """Initialize progress bar for simulation.""" + if not TQDM_AVAILABLE: + return + + # Determine progress total based on configuration + if self.config.max_ticks: + total = self.config.max_ticks + unit = 'ticks' + elif self.config.max_duration: + total = int(self.config.max_duration) + unit = 'sec' + else: + # No clear total - create indeterminate progress bar + total = None + unit = 'ticks' + + if total: + self.progress_bar = tqdm( + total=total, + unit=unit, + desc="Simulation", + leave=True, # Keep the bar when done + bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]' + ) + else: + self.progress_bar = tqdm( + unit='ticks', + desc="Simulation", + leave=True, + bar_format='{l_bar}{bar}| {n_fmt} [{elapsed}, {rate_fmt}]' + ) + + def _update_progress_bar(self): + """Update progress bar with current status.""" + current_time = time.time() + if current_time - self.last_progress_update < self.progress_update_interval: + return + + current_tick = self.simulation_core.state.total_ticks + tps = self.simulation_core.state.actual_tps + elapsed = current_time - self.start_time + + if TQDM_AVAILABLE and self.progress_bar: + # Use tqdm progress bar + if self.config.max_ticks: + # Update based on tick progress + progress = min(current_tick, self.config.max_ticks) + self.progress_bar.n = progress + self.progress_bar.set_postfix({ + 'TPS': f'{tps:.1f}', + 'Files': self.files_written + }) + elif self.config.max_duration: + # Update based on elapsed time + progress = min(elapsed, self.config.max_duration) + self.progress_bar.n = int(progress) + self.progress_bar.set_postfix({ + 'TPS': f'{tps:.1f}', + 'Files': self.files_written, + 'Tick': current_tick + }) + else: + # Indeterminate progress + self.progress_bar.n = current_tick + self.progress_bar.set_postfix({ + 'TPS': f'{tps:.1f}', + 'Files': self.files_written + }) + + self.progress_bar.refresh() + else: + # Simple text-based progress + eta_text = "" + if self.config.max_ticks and current_tick > 0: + tick_rate = current_tick / elapsed if elapsed > 0 else 0 + remaining_ticks = self.config.max_ticks - current_tick + eta_seconds = remaining_ticks / tick_rate if tick_rate > 0 else 0 + eta_minutes, eta_seconds = divmod(eta_seconds, 60) + eta_text = f"ETA: {int(eta_minutes)}m{int(eta_seconds)}s" + elif self.config.max_duration: + remaining_seconds = self.config.max_duration - elapsed + eta_minutes, eta_seconds = divmod(remaining_seconds, 60) + eta_text = f"ETA: {int(eta_minutes)}m{int(eta_seconds)}s" + + # Calculate progress percentage if we have a limit + progress_pct = "" + if self.config.max_ticks: + pct = (current_tick / self.config.max_ticks) * 100 + progress_pct = f"{pct:.1f}%" + elif self.config.max_duration: + pct = (elapsed / self.config.max_duration) * 100 + progress_pct = f"{pct:.1f}%" + + progress_line = f"[{current_time - self.start_time:.1f}s] " + if progress_pct: + progress_line += f"Progress: {progress_pct} " + progress_line += f"Tick: {current_tick} TPS: {tps:.1f} Files: {self.files_written}" + if eta_text: + progress_line += f" {eta_text}" + + # Overwrite the previous line (using carriage return) + print(f"\r{progress_line}", end="", flush=True) + + self.last_progress_update = current_time + + def _close_progress_bar(self): + """Close the progress bar.""" + if not TQDM_AVAILABLE and self.running: + # Print a newline to clear the text progress line + print() + elif TQDM_AVAILABLE and self.progress_bar: + self.progress_bar.close() + def run(self) -> Dict[str, Any]: """Run the headless simulation.""" # Determine if we should run at max speed @@ -107,6 +232,9 @@ class HeadlessSimulationEngine: self.start_time = time.time() self.simulation_core.start() + # Initialize progress bar + self._init_progress_bar() + # Enable sprint mode for maximum speed if not real-time mode if max_speed_mode: self.simulation_core.timing.set_sprint_mode(True) @@ -137,6 +265,9 @@ class HeadlessSimulationEngine: self._write_batch_data() last_batch_time = time.time() + # Update progress bar + self._update_progress_bar() + # Real-time delay if needed if self.config.real_time: time.sleep(0.016) # ~60 FPS @@ -206,6 +337,7 @@ class HeadlessSimulationEngine: } formatted_data = formatter.format(combined_data) self.file_writer.write(formatted_data, filename) + self.files_written += 1 # Clear written data data_list.clear() @@ -214,6 +346,10 @@ class HeadlessSimulationEngine: def _finalize(self): """Finalize simulation and write remaining data.""" + + # Close progress bar + self._close_progress_bar() + print("Finalizing simulation...") # Write any remaining data @@ -224,12 +360,14 @@ class HeadlessSimulationEngine: if 'json' in self.formatters: summary_data = self.formatters['json'].format(summary) self.file_writer.write(summary_data, "simulation_summary.json") + self.files_written += 1 # Stop simulation self.simulation_core.stop() self.file_writer.close() print("Simulation completed") + print(f"Total files written: {self.files_written}") def _get_summary(self) -> Dict[str, Any]: """Get simulation summary."""