Source code for src.cache.cache_helpers
from __future__ import annotations
import datetime # TODO: UTC?
import math
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from src.runs import RunParser
[docs]
class Statistic:
def __init__(self, *, set_default: bool = False):
self.all_character_count = None
self.ironclad_count = None
self.silent_count = None
self.defect_count = None
self.watcher_count = None
if set_default:
self.all_character_count = 0
self.ironclad_count = 0
self.silent_count = 0
self.defect_count = 0
self.watcher_count = 0
def __str__(self) -> str:
return f'all_character_count: {self.all_character_count}, IC: {self.ironclad_count}, Silent: {self.silent_count}, Defect: {self.defect_count}, Watcher: {self.watcher_count}'
[docs]
class RunStats:
def __init__(self):
self._is_loaded = False
self.current_year = datetime.datetime.now().year
self.all_wins = Statistic(set_default=True)
self.all_losses = Statistic(set_default=True)
self.year_wins: dict[int, Statistic] = {
self.current_year: Statistic(set_default=True)
}
self.year_losses: dict[int, Statistic] = {
self.current_year: Statistic(set_default=True)
}
self.pb = Statistic(set_default=True)
self.streaks = Statistic()
self.last_timestamp: datetime.datetime = None
def __str__(self) -> str:
return f'all_wins: {self.all_wins}\nall_losses: {self.all_losses}\nstreaks: {self.streaks}'
@property
def is_loaded(self) -> bool:
return self._is_loaded
@is_loaded.setter
def is_loaded(self, val):
self._is_loaded = val
@property
def date_range_string(self) -> str:
return "all-time"
[docs]
def add_win(self, char: str, run_date: datetime):
date = run_date.date()
if not date.year in self.year_wins:
self.year_wins[date.year] = Statistic(set_default=True)
self._increment_stat(self.all_wins, char)
self._increment_stat(self.year_wins[date.year], char)
[docs]
def add_loss(self, char: str, run_date: datetime):
date = run_date.date()
if not date.year in self.year_losses:
self.year_losses[date.year] = Statistic(set_default=True)
self._increment_stat(self.all_losses, char)
self._increment_stat(self.year_losses[date.year], char)
[docs]
def check_pb(self, run: RunParser):
if not run.modded:
if self.pb.all_character_count < run.rotating_streak.streak:
self.pb.all_character_count = run.rotating_streak.streak
match run.character:
case "Ironclad":
if self.pb.ironclad_count < run.character_streak.streak:
self.pb.ironclad_count = run.character_streak.streak
case "Silent":
if self.pb.silent_count < run.character_streak.streak:
self.pb.silent_count = run.character_streak.streak
case "Defect":
if self.pb.defect_count < run.character_streak.streak:
self.pb.defect_count = run.character_streak.streak
case "Watcher":
if self.pb.watcher_count < run.character_streak.streak:
self.pb.watcher_count = run.character_streak.streak
def _increment_stat(self, stat: Statistic, char: str):
match char:
case "Ironclad":
stat.ironclad_count += 1
case "Silent":
stat.silent_count += 1
case "Defect":
stat.defect_count += 1
case "Watcher":
stat.watcher_count += 1
stat.all_character_count += 1
[docs]
def clear(self):
self.__init__()
[docs]
class RunStatsByDate(RunStats):
def __init__(self):
super().__init__()
self.start_date = None
self.end_date = None
@property
def date_range_string(self) -> str:
date_str = ""
format = "%Y/%m/%d"
if self.start_date is None and self.end_date is None:
date_str = "all-time"
elif self.start_date is not None and self.end_date is None:
date_str = f'starting with {self.start_date.strftime(format)}'
elif self.start_date is None and self.end_date is not None:
date_str = f'before {self.end_date.strftime(format)}'
else:
date_str = f'{self.start_date.strftime(format)} - {self.end_date.strftime(format)}'
return date_str
[docs]
class RunLinkedListNode:
def __init__(self):
self.next: RunParser = None
self.prev: RunParser = None
self.next_char: RunParser = None
self.prev_char: RunParser = None
self.next_win: RunParser = None
self.prev_win: RunParser = None
self.next_loss: RunParser = None
self.prev_loss: RunParser = None
[docs]
def get_run(self, *, is_prev: bool, is_character_specific: bool) -> RunParser:
if is_prev:
if is_character_specific:
return self.prev_char
else:
return self.prev
else:
if is_character_specific:
return self.next_char
else:
return self.next
[docs]
class MasteryStats:
def __init__(self) -> None:
self.mastered_cards: dict[str, RunParser] = {}
self.mastered_relics: dict[str, RunParser] = {}
self.colors: dict[str, str] = {}
self.last_run_timestamp: datetime.datetime = None
[docs]
class StreakCache:
"""
A streak superclass that contains StreakContainer objects.
"""
def __init__(self, since: datetime.datetime):
self.since = since
self.containers = []
def __repr__(self):
return f"StreakCache of {len(self.containers)}: {self.containers}"
[docs]
def clear(self):
self.containers = []
[docs]
def prepend(self, containers):
self.containers.insert(0, containers)
@property
def latest(self):
return self.containers[0]
[docs]
class StreakContainer:
"""
Collection of runs that form a winning or losing streak.
The `winning_streak` attribute denotes if the streak contains wins
or not. The notion of losing streaks are used on the streak page to
show how many runs were between the streaks.
The `ongoing` attribute denotes if the streak is still going. If
it's True, this is shown on the streak date in the UI.
The `runs` attribute is the list of runs in the streak. If the
streak is over, the losing run that broke it should be in there as
well so it can be shown in the UI as the one that broke it.
"""
def __init__(self, winning_streak: bool, runs):
self.winning_streak = winning_streak
self.ongoing = False
self.runs = runs
def __repr__(self):
return f"Streaks<{self.character} {self.verb} of {self.length}>"
[docs]
def append(self, runs):
self.runs += runs
[docs]
def get_run(self, x):
return self.display_runs[x]
@property
def display_runs(self):
"""
The runs, but with possible added context runs:
- The losing run included if this is a winning streak that is over.
- The current run if we are live and the stream is ongoing.
"""
if self.ongoing:
from src.save import _savefile
if _savefile.character is not None and _savefile.character == self.character:
return self.runs + [_savefile]
return self.runs
return self.runs + [self.runs[-1].matched.next_char]
@property
def start(self):
return self.runs[0].timestamp.strftime("%b %-d")
@property
def end(self):
# We use 'display_runs' here so that if the streak is over we show the date of the run that
# broke the streak.
return self.display_runs[-1].timestamp.strftime("%b %-d")
@property
def runs_url(self):
start = self.runs[0].timestamp.strftime("%s")
end = self.runs[-1].timestamp.strftime("%s")
return f"/profile/0/runs/by-timestamp/{start}..{end}"
@property
def character(self):
return self.runs[0].character
@property
def verb(self):
return self.runs[0].verb
@property
def length(self):
return len(self.display_runs)
@property
def target(self):
"""
The amount of blobs to show for this group.
If less than 20, show 20.
After 20, show ten more for every ten passed.
"""
if self.length <= 20:
return 20
return 10 * (math.floor(self.length / 10) + 1)
@property
def streak(self):
"""
Counts the amount of runs that were the actual streak.
Without this, the streak would show one too many once it's over
since we care to show the losing run inside the streak
collection.
"""
return len([x for x in self.runs if x.won])