text: import math
  
  from necrobot.database import matchdb, racedb
  from necrobot.util import console, racetime
  
  from necrobot.stats.leaguestats import LeagueStats
  from necrobot.util.character import NDChar
  from necrobot.util.singleton import Singleton
  
  
  class CharacterStats(object):
      def __init__(self, ndchar):
          self._ndchar = ndchar
          self.number_of_races = 0
          self.mean = 0
          self.var = 0
          self.winrate = 0
          self.has_wins = False
  
      @property
      def ndchar(self) -> NDChar:
          return self._ndchar
  
      @property
      def charname(self) -> str:
          return self.ndchar.name
  
      @property
      def stdev(self) -> float:
          return math.sqrt(self.var)
  
      @property
      def mean_str(self) -> str:
          if self.has_wins:
              return racetime.to_str(int(self.mean))
          else:
              return '--'
  
      @property
      def stdev_str(self) -> str:
          if self.has_wins:
              return racetime.to_str(int(self.stdev))
          else:
              return '--'
  
      def barf(self) -> None:
          console.info('{0:>10}   {1:>5}   {2:>9}  {3:>9}  {4:>6}\n'.format(
              self.charname,
              self.number_of_races,
              self.mean_str,
              self.stdev_str,
              int(self.winrate * 100)))
  
  
  class GeneralStats(object):
      def __init__(self):
          self._charstats = []
  
      @property
      def infotext(self) -> str:
          info_text = '{0:>10}   {1:<5}   {2:<9}  {3:<9}  {4}\n'.format('', 'Races', 'Avg', 'Stdev', 'Clear%')
          for char in sorted(self._charstats, key=lambda c: c.number_of_races, reverse=True):
              info_text += '{0:>10}   {1:>5}   {2:>9}  {3:>9}  {4:>6}\n'.format(
                  char.charname,
                  char.number_of_races,
                  char.mean_str,
                  char.stdev_str,
                  int(char.winrate*100))
          return info_text[:-1]
  
      def insert_charstats(self, char: CharacterStats) -> None:
          self._charstats.append(char)
  
      def get_charstats(self, char: NDChar) -> CharacterStats:
          for c in self._charstats:
              if c.ndchar == char:
                  return c
          return CharacterStats(char)
  
  
  class StatCache(object, metaclass=Singleton):
      class CachedStats(object):
          def __init__(self):
              self.last_race_number_amplified = 0      # The number of the last race when amplified was cached
              self.last_race_number_base = 0           # The number of the last race when base was cached
              self.amplified_stats = GeneralStats()
              self.base_stats = GeneralStats()
  
      def __init__(self):
          self._cache = {}  # Map from discord ID's to UserStats
  
      async def get_general_stats(self, user_id, amplified) -> GeneralStats:
          last_race_number = await racedb.get_largest_race_number(user_id=user_id)
  
          # Check whether we have an up-to-date cached version, and if so, return it
          cached_data = self.CachedStats()
          if user_id in self._cache:
              cached_data = self._cache[user_id]
              if amplified:
                  if cached_data.last_race_number_amplified == last_race_number:
                      return cached_data.amplified_stats
              else:
                  if cached_data.last_race_number_base == last_race_number:
                      return cached_data.base_stats
  
          # If here, the cache is out-of-date
          general_stats = GeneralStats()
          for row in await racedb.get_allzones_race_numbers(user_id=user_id, amplified=amplified):
              char = NDChar.fromstr(row[0])
              charstats = CharacterStats(char)
              charstats.number_of_races = int(row[1])
              total_time = 0
              total_squared_time = 0
              number_of_wins = 0
              number_of_forfeits = 0
              for stat_row in await racedb.get_all_racedata(user_id=user_id, char_name=char.name, amplified=amplified):
                  if int(stat_row[1]) == -2:  # finish
                      time = int(stat_row[0])
                      total_time += time
                      total_squared_time += time * time
                      number_of_wins += 1
                  else:
                      number_of_forfeits += 1
  
              if number_of_wins > 0:
                  charstats.mean = total_time / number_of_wins
  
              if number_of_wins > 1:
                  charstats.has_wins = True
                  charstats.var = \
                      (total_squared_time / (number_of_wins-1)) - charstats.mean * total_time/(number_of_wins-1)
  
              if number_of_wins + number_of_forfeits > 0:
                  charstats.winrate = number_of_wins / (number_of_wins + number_of_forfeits)
  
              general_stats.insert_charstats(charstats)
  
          # Update the cache
          if amplified:
              cached_data.last_race_number_amplified = last_race_number
              cached_data.amplified_stats = general_stats
          else:
              cached_data.last_race_number_base = last_race_number
              cached_data.base_stats = general_stats
          self._cache[user_id] = cached_data
  
          # Return
          return general_stats
  
  
  async def get_general_stats(user_id: int, amplified: bool) -> GeneralStats:
      return await StatCache().get_general_stats(user_id, amplified)
  
  
  async def get_character_stats(user_id: int, ndchar: NDChar, amplified: bool) -> CharacterStats:
      general_stats = await StatCache().get_general_stats(user_id, amplified)
      return general_stats.get_charstats(ndchar)
  
  
  async def get_winrates(user_id_1: int, user_id_2: int, ndchar: NDChar, amplified: bool) -> tuple or None:
      stats_1 = await get_character_stats(user_id_1, ndchar, amplified)
      stats_2 = await get_character_stats(user_id_2, ndchar, amplified)
      if not stats_1.has_wins or not stats_2.has_wins:
          return None
  
      m2_minus_m1 = stats_2.mean - stats_1.mean
      sum_var = stats_1.var + stats_2.var
      erf_arg = m2_minus_m1 / math.sqrt(2*sum_var)
      if m2_minus_m1 > 0:
          winrate_of_1_if_both_finish = (1.0 + math.erf(erf_arg))/2.0
      else:
          winrate_of_1_if_both_finish = (1.0 - math.erf(-erf_arg))/2.0
  
      both_finish_prob = stats_1.winrate * stats_2.winrate
      neither_finish_prob = (1-stats_1.winrate)*(1-stats_2.winrate)
      winrate_of_1 = winrate_of_1_if_both_finish*both_finish_prob + (stats_1.winrate - both_finish_prob)
      winrate_of_2 = (1.0-winrate_of_1_if_both_finish)*both_finish_prob + (stats_2.winrate - both_finish_prob)
      return winrate_of_1, winrate_of_2, neither_finish_prob
  
  
  async def get_most_races_infotext(ndchar: NDChar, limit: int) -> str:
      most_races = await racedb.get_most_races_leaderboard(str(ndchar), limit)
      infotext = '{0:>16} {1:>6} {2:>6}\n'.format('', 'Base', 'Amp')
      for row in most_races:
          infotext += '{0:>16} {1:>6} {2:>6}\n'.format(row[0], row[2], row[3])
      return infotext
  
  
  async def get_fastest_times_infotext(ndchar: NDChar, amplified: bool, limit: int) -> str:
      fastest_times = await racedb.get_fastest_times_leaderboard(str(ndchar), amplified, limit)
      infotext = '{0:>16} {1:<9} {2:<9} {3:<13}\n'.format('', 'Time (rta)', 'Seed', 'Date')
      for row in fastest_times:
          infotext += '{0:>16} {1:>9} {2:>9} {3:>13}\n'.format(
              row[0],
              racetime.to_str(int(row[1])),
              row[2],
              row[3].strftime("%b %d, %Y"))
      return infotext
  
  
  async def get_fastest_times_league_infotext(limit: int) -> str:
      fastest_times = await matchdb.get_fastest_wins_raw(limit)
      max_namelen = 0
      for row in fastest_times:
          max_namelen = max(max_namelen, len(row[1]))
  
      dated_format_str = '  {winner:>' + str(max_namelen) + '} -- {time:<9} (vs {loser}, {date:%b %d})\n'
      undated_format_str = '  {winner:>' + str(max_namelen) + '} -- {time:<9} (vs {loser})\n'
      infotext = ''
      for row in fastest_times:
          if row[3] is not None:
              infotext += dated_format_str.format(
                  winner=row[1],
                  time=racetime.to_str(row[0]),
                  loser=row[2],
                  date=row[3]
              )
          else:
              infotext += undated_format_str.format(
                  winner=row[1],
                  time=racetime.to_str(row[0]),
                  loser=row[2]
              )
      return infotext[:-1] if infotext else ''
  
  
  async def get_league_stats(user_id: int) -> LeagueStats:
      stats = await matchdb.get_matchstats_raw(user_id)
      return LeagueStats(
          wins=stats[0],
          best=stats[1],
          average=stats[2],
          losses=stats[3]
      )
  