1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
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([pow(i + 1, 2) 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
|