import math import numpy as np import time import threading import inspect from beautifultable import BeautifulTable from logger import Logger from config import Config from messages import Messenger from utils.misc import hms from utils.levels import get_level from version import __version__ from ui import player_bar class GameStats: def __init__(self): self._messenger = Messenger() self._start_time = time.time() self._timer = None self._timepaused = None self._paused = False self._game_counter = 0 self._chicken_counter = 0 self._death_counter = 0 self._merc_death_counter = 0 self._runs_failed = 0 self._run_counter = 1 self._consecutive_runs_failed = 0 self._failed_game_time = 0 self._location = None self._location_stats = {} self._location_stats["totals"] = { "items": 0, "deaths": 0, "chickens": 0, "merc_deaths": 0, "failed_runs": 0 } self._stats_filename = f'stats_{time.strftime("%Y%m%d_%H%M%S")}.log' self._nopickup_active = False self._starting_exp = 0 self._current_exp = 1 self._current_lvl = 0 def update_location(self, loc: str): if self._location != loc: self._location = str(loc) self.populate_location_stat() def populate_location_stat(self): if self._location not in self._location_stats: self._location_stats[self._location] = { "items": [], "deaths": 0, "chickens": 0, "merc_deaths": 0, "failed_runs": 0 } def log_item_keep(self, item_name: str, send_message: bool, img: np.ndarray, ocr_text: str = '', expression: str = '', item_props: dict = {}): filtered_substrings = [" POTION", " OF IDENTIFY", " OF TOWN PORTAL", " AMETHYST", " RUBY", " TOPAZ", " EMERALD", " SAPPHIRE", " DIAMOND"] filtered_matches = ["DIAMOND", "AMETHYST", "RUBY", "TOPAZ", "EMERALD", "SAPPHIRE", "ARROWS", "BOLTS", "CHIPPED SKULL", "FLAWED SKULL", "SKULL", "FLAWLESS SKULL", "PERFECT SKULL"] skip_log = any(substring in item_name for substring in filtered_substrings) or any(match == item_name.strip() for match in filtered_matches) if self._location is not None and not skip_log: Logger.debug(f"Stashed and logged: {item_name}") self._location_stats[self._location]["items"].append(item_name) self._location_stats["totals"]["items"] += 1 if send_message and self._messenger.enabled and not skip_log: if expression[0] != "@": self._messenger.send_item(item_name, img, self._location, ocr_text, expression, item_props) def log_death(self, img: str): self._death_counter += 1 if self._location is not None: self._location_stats[self._location]["deaths"] += 1 self._location_stats["totals"]["deaths"] += 1 if self._messenger.enabled: self._messenger.send_death(self._location, img) def log_chicken(self, img: str): self._chicken_counter += 1 if self._location is not None: self._location_stats[self._location]["chickens"] += 1 self._location_stats["totals"]["chickens"] += 1 if Config().general["discord_log_chicken"] and self._messenger.enabled: self._messenger.send_chicken(self._location, img) def log_merc_death(self): self._merc_death_counter += 1 if self._location is not None: self._location_stats[self._location]["merc_deaths"] += 1 self._location_stats["totals"]["merc_deaths"] += 1 def log_start_game(self): if self._game_counter > 0: self._save_stats_to_file() if Config().general["discord_status_count"] and self._game_counter % Config().general["discord_status_count"] == 0: # every discord_status_count game send a message update about current status self._send_status_update() self._game_counter += 1 self._timer = time.time() Logger.info(f"Starting game #{self._game_counter}") def log_end_game(self, failed: bool = False): elapsed_time = 0 if self._timer is not None: elapsed_time = time.time() - self._timer self._timer = None if failed: self._runs_failed += 1 self._consecutive_runs_failed += 1 if self._location is not None: self._location_stats[self._location]["failed_runs"] += 1 self._location_stats["totals"]["failed_runs"] += 1 self._failed_game_time += elapsed_time Logger.warning(f"End failed game: Elapsed time: {elapsed_time:.2f}s Fails: {self._consecutive_runs_failed}") else: self._consecutive_runs_failed = 0 Logger.info(f"End game. Elapsed time: {elapsed_time:.2f}s") def log_exp(self): exp = player_bar.get_experience() if exp[1] > 0: curr_lvl = get_level(exp[1])['lvl'] if curr_lvl > 0: self._current_lvl = curr_lvl-1 if self._starting_exp == 0: self._starting_exp = exp[0] if exp[0] > 0: self._current_exp = exp[0] def pause_timer(self): if self._timer is None or self._paused: return self._timepaused = time.time() self._paused = True def resume_timer(self): if self._timer is None or not self._paused: return pausetime = time.time() - self._timepaused self._timer = self._timer + pausetime self._paused = False def get_current_game_length(self): if self._timer is None: return 0 if self._paused: return self._timepaused - self._timer else: return time.time() - self._timer def get_consecutive_runs_failed(self): return self._consecutive_runs_failed def _create_msg(self): elapsed_time = time.time() - self._start_time elapsed_time_str = hms(elapsed_time) avg_length_str = "n/a" 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) msg = f'\nSession length: {elapsed_time_str}' msg += f'\nGames: {self._game_counter}' msg += f'\nAvg Game Length: {avg_length_str}' msg += f'\nCurrent Level: {curr_lvl["lvl"]}' if curr_lvl["lvl"] < 99: try: exp_gained = self._current_exp - curr_lvl['exp'] per_to_lvl = exp_gained / curr_lvl["xp_to_next"] gained_exp = self._current_exp - self._starting_exp exp_per_second = gained_exp / good_games_time exp_per_hour = round(exp_per_second * 3600, 1) exp_per_game = round(gained_exp / float(good_games_count), 1) exp_needed = curr_lvl['xp_to_next'] - exp_gained time_to_lvl = exp_needed / exp_per_second games_to_lvl = exp_needed / exp_per_game msg += f'\nPercent to Level: {math.ceil(per_to_lvl*100)}%' msg += f'\nXP Gained: {gained_exp:,}' msg += f'\nXP Per Hour: {exp_per_hour:,}' msg += f'\nXP Per Game: {exp_per_game:,}' msg += f'\nTime Needed To Level: {hms(time_to_lvl)}' msg += f'\nGames Needed To Level: {math.ceil(games_to_lvl):,}' except: Logger.warning("Failed to log exp") table = BeautifulTable() table.set_style(BeautifulTable.STYLE_BOX_ROUNDED) for location in self._location_stats: if location == "totals": continue stats = self._location_stats[location] table.rows.append([location, len(stats["items"]), stats["chickens"], stats["deaths"], stats["merc_deaths"], stats["failed_runs"]]) table.rows.append([ "T", self._location_stats["totals"]["items"], self._location_stats["totals"]["chickens"], self._location_stats["totals"]["deaths"], self._location_stats["totals"]["merc_deaths"], self._location_stats["totals"]["failed_runs"] ]) table.columns.header = ["Run", "I", "C", "D", "MD", "F"] msg += f"\n{str(table)}\n" return msg def _send_status_update(self): msg = f"Status Report\n{self._create_msg()}\nVersion: {__version__}" if self._messenger.enabled: self._messenger.send_message(msg) def _save_stats_to_file(self): msg = self._create_msg() msg += "\nItems:" for location in self._location_stats: if location == "totals": continue stats = self._location_stats[location] msg += f"\n {location}:" for item_name in stats["items"]: msg += f"\n {item_name}" with open(file=f"log/stats/{self._stats_filename}", mode="w+", encoding="utf-8") as f: f.write(msg) if __name__ == "__main__": game_stats = GameStats() game_stats.log_item_keep("rune_12", True) game_stats._save_stats_to_file()