diff --git a/src/game_stats.py b/src/game_stats.py index 6d74995..2107599 100644 --- a/src/game_stats.py +++ b/src/game_stats.py @@ -507,6 +507,116 @@ class GameStats: with open(file=f"log/stats/{self._mini_stats_filename}", mode="w+", encoding="utf-8") as f: json.dump(payload, f, indent=2) + def _save_session_report(self): + """Save a session report to log/runs/ with date-based rotation.""" + os.makedirs("log/runs", exist_ok=True) + elapsed_time = time.time() - self._start_time + elapsed_time_str = hms(elapsed_time) + good_games_count = self._game_counter - self._runs_failed + good_games_time = elapsed_time - self._failed_game_time + if good_games_count == 0: + good_games_count = 1 + avg_length = good_games_time / float(good_games_count) + avg_length_str = hms(avg_length) + + curr_lvl = get_level(self._current_lvl) if self._current_lvl > 0 else {"lvl": 0, "exp": 0, "xp_to_next": 0} + + lines = [] + lines.append("=" * 50) + lines.append("BOTTY SESSION REPORT") + lines.append("=" * 50) + lines.append(f"Date: {time.strftime('%Y-%m-%d %H:%M:%S')}") + lines.append(f"Version: {__version__}") + lines.append(f"Session: {elapsed_time_str}") + lines.append(f"Games: {self._game_counter}") + lines.append(f"Failed: {self._runs_failed}") + lines.append(f"Avg Game: {avg_length_str}") + lines.append(f"Level: {curr_lvl['lvl'] if curr_lvl['lvl'] > 0 else 'n/a'}") + lines.append(f"Runes: {self._location_stats['totals']['runes']}") + lines.append(f"Est FG: {self._location_stats['totals']['fg_estimated']:.1f}") + lines.append(f"Gold Stashed (Session): {self._format_gold(self._location_stats['totals']['gold_stashed'])}") + lines.append(f"Gold In Stash: {self._format_gold(self._gold_in_stash)}") + lines.append(f"Deaths: {self._death_counter}") + lines.append(f"Chickens: {self._chicken_counter}") + lines.append(f"Merc Deaths:{self._merc_death_counter}") + + if curr_lvl["lvl"] > 0 and curr_lvl["lvl"] < 99 and self._current_exp > 0: + gained_exp = self._current_exp - self._starting_exp + if good_games_time > 0: + exp_per_hour = round((gained_exp / good_games_time) * 3600, 1) + lines.append(f"XP Gained: {gained_exp:,}") + lines.append(f"XP/Hour: {exp_per_hour:,}") + + lines.append("") + lines.append("Per-Run Stats:") + lines.append(f"{'Run':<20} {'Items':>6} {'Chick':>5} {'Death':>5} {'MercD':>5} {'Fail':>4} {'Gold':>14}") + lines.append("-" * 60) + for location in self._location_stats: + if location == "totals": + continue + stats = self._location_stats[location] + lines.append(f"{location:<20} {len(stats['items']):>6} {stats['chickens']:>5} {stats['deaths']:>5} {stats['merc_deaths']:>5} {stats['failed_runs']:>4} {self._format_gold(stats.get('gold_stashed', 0)):>14}") + totals = self._location_stats["totals"] + total_items = sum(len(self._location_stats[loc]["items"]) for loc in self._location_stats if loc != "totals") + lines.append("-" * 60) + lines.append(f"{'TOTAL':<20} {total_items:>6} {totals['chickens']:>5} {totals['deaths']:>5} {totals['merc_deaths']:>5} {totals['failed_runs']:>4} {self._format_gold(totals['gold_stashed']):>14}") + + if len(self._fg_item_totals) > 0: + lines.append("") + lines.append("FG Tracker (Session Totals):") + sorted_fg = sorted(self._fg_item_totals.items(), key=lambda x: x[1], reverse=True) + for fg_name, fg_total in sorted_fg: + count = self._fg_item_counts.get(fg_name, 0) + lines.append(f" {fg_name}: {count}x, {fg_total:.1f} fg") + + lines.append("") + lines.append("=" * 50) + + report_content = "\n".join(lines) + + # Save with date-based filename + report_filename = f"run_{time.strftime('%Y%m%d_%H%M%S')}.txt" + report_path = f"log/runs/{report_filename}" + with open(report_path, "w", encoding="utf-8") as f: + f.write(report_content) + + Logger.info(f"Session report saved to {report_path}") + + # Also save a JSON version for easy parsing + json_payload = { + "ts": time.strftime("%Y-%m-%dT%H:%M:%S"), + "version": __version__, + "session_seconds": round(elapsed_time, 2), + "games": self._game_counter, + "runs_failed": self._runs_failed, + "avg_game_length": round(avg_length, 2), + "current_level": self._current_lvl if self._current_lvl > 0 else None, + "runes_kept": self._location_stats["totals"]["runes"], + "estimated_fg": round(self._location_stats["totals"]["fg_estimated"], 1), + "gold_stashed_total": self._gold_stashed_total, + "gold_in_stash": self._gold_in_stash, + "deaths": self._death_counter, + "chickens": self._chicken_counter, + "merc_deaths": self._merc_death_counter, + "xp_gained": self._current_exp - self._starting_exp if self._current_exp > 0 else 0, + "locations": {} + } + for location in self._location_stats: + if location == "totals": + continue + stats = self._location_stats[location] + json_payload["locations"][location] = { + "items": len(stats["items"]), + "chickens": stats["chickens"], + "deaths": stats["deaths"], + "merc_deaths": stats["merc_deaths"], + "failed_runs": stats["failed_runs"], + "gold_stashed": stats.get("gold_stashed", 0) + } + json_path = f"log/runs/{report_filename.replace('.txt', '.json')}" + with open(json_path, "w", encoding="utf-8") as f: + json.dump(json_payload, f, indent=2) + if __name__ == "__main__": game_stats = GameStats() diff --git a/src/logger.py b/src/logger.py index a0d2c9c..5df74e1 100644 --- a/src/logger.py +++ b/src/logger.py @@ -5,6 +5,7 @@ import sys import threading import traceback import warnings +from logging.handlers import TimedRotatingFileHandler from version import __version__ from colorama import Fore, Back, Style, init import time @@ -118,8 +119,14 @@ class Logger: Logger.console_handler = logging.StreamHandler(sys.stdout) Logger.console_handler.setLevel(Logger._logger_level) - # Setup the file handler - Logger.file_handler = logging.FileHandler(Logger._current_log_file_path, 'a') + # Setup the file handler (rotating — daily, keeps 7 days) + Logger.file_handler = TimedRotatingFileHandler( + Logger._current_log_file_path, + when='midnight', + interval=1, + backupCount=7, + encoding='utf-8' + ) Logger.file_handler.setLevel(Logger._logger_level) # Optionally add a formatter diff --git a/src/main.py b/src/main.py index 34fc97f..7eb06b7 100644 --- a/src/main.py +++ b/src/main.py @@ -52,8 +52,14 @@ def start_or_stop_graphic_debugger(controllers: Controllers): controllers.game.stop() controllers.debugger.start() -def on_exit(): - Logger.info(f'Force Exit') +def on_exit(controllers: Controllers): + Logger.info('Force Exit') + # Generate session report before exiting + if controllers.game.game_stats: + try: + controllers.game.game_stats._save_session_report() + except Exception as e: + Logger.warning(f"Failed to save session report: {e}") screen.stop_detecting_window() restore_d2r_window_visibility() os._exit(1) @@ -73,7 +79,7 @@ def startup_checks(): def main(): # Create folder for debug screenshots if they dont exist yet - for dir_name in ["log", "log/stats", "log/screenshots", "log/screenshots/info", "log/screenshots/items", "log/screenshots/pickit", "log/screenshots/generated"]: + for dir_name in ["log", "log/stats", "log/runs", "log/screenshots", "log/screenshots/info", "log/screenshots/items", "log/screenshots/pickit", "log/screenshots/generated"]: os.makedirs(dir_name, exist_ok=True) controllers = Controllers( @@ -125,7 +131,7 @@ def main(): keyboard.add_hotkey(Config().advanced_options['restore_settings_from_backup_key'], lambda: restore_settings_from_backup()) keyboard.add_hotkey(Config().advanced_options['settings_backup_key'], lambda: backup_settings()) keyboard.add_hotkey(Config().advanced_options['resume_key'], lambda: start_or_pause_bot(controllers)) - keyboard.add_hotkey(Config().advanced_options["exit_key"], lambda: on_exit()) + keyboard.add_hotkey(Config().advanced_options["exit_key"], lambda: on_exit(controllers)) keyboard.wait()