from random import shuffle from .scheduler import Scheduler from .card import Card HISTORY_DEPTH = 8 class SchedulerBrutal(Scheduler): """ The brutal scheduler tracks how well the player has consolidated each card and also how often the card has been shown. Using this information, it prioritizes cards that have been shown less frequently and recently, which means the player will often see totally new cards in test sessions. """ def __init__(self, cards: dict[str, Card], state: dict): self._cards = cards self._state = {} # Synchronise state with current card collection for id, card in self._cards.items(): history = state.get(id, [None] * HISTORY_DEPTH) # Adjust history if depth has changed if len(history) > HISTORY_DEPTH: history = history[-HISTORY_DEPTH:] elif len(history) < HISTORY_DEPTH: history = ([None] * (HISTORY_DEPTH - len(history))) + history self._state[id] = history def practice(self, size: int) -> list[str]: return self._schedule(size) def test(self, size: int) -> list[str]: return self._schedule(size) def update(self, results: dict[str, int]) -> None: # Add card result to sliding window, or None if card was not shown self._state = {id: history[1:] + [results.get(id, None)] for id, history in self._state.items()} def getState(self) -> dict: return self._state @staticmethod def _consolidationIndex(history: list, weights: range) -> float: """ Consolidation index is a measure of how well the player has guessed the card recently """ relevant_history = [(h, w) for h, w in zip(history, weights) if h is not None] weighted_history = sum([h * w for h, w in relevant_history]) total_weights = sum([w for h, w in relevant_history]) return weighted_history / total_weights if total_weights > 0 else 0.0 @staticmethod def _exposureIndex(history: list) -> float: """ Exposure index is a measure of how much and how recently a card has been shown """ return sum([i + 1 for i, h in enumerate(history) if h is not None]) def _schedule(self, size: int) -> list[str]: weights = range(10, 10 + HISTORY_DEPTH) cards = [id for id, card in self._cards.items()] # Shuffle once at the start, that way if all cards have the same score you still get some variety shuffle(cards) # First sort by consolidation index cards.sort(key=lambda id: SchedulerBrutal._consolidationIndex(self._state[id], weights)) # Next sort by exposure index cards.sort(key=lambda id: SchedulerBrutal._exposureIndex(self._state[id])) # Return least exposed and least consolidated cards, shuffled cards = cards[0:size] shuffle(cards) return cards