""" https://gist.github.com/riceissa/1ead1b9881ffbb48793565ce69d7dbdd """ # "New Cards" tab NEW_STEPS = [15, 25, 35] # in minutes GRADUATING_INTERVAL = 15 # in days EASY_INTERVAL = 4 # in days STARTING_EASE = 2.50 # in percent # "Reviews" tab EASY_BONUS = 1.30 INTERVAL_MODIFIER = 1 MAXIMUM_INTERVAL = 36500 # in days # "Lapses" tab LAPSES_STEPS = [20] # in minutes NEW_INTERVAL = 0.70 MINIMUM_INTERVAL = 2 # in days class Card: def __init__(self): self.status = 'learning' # can be 'learning', 'learned', or 'relearning' self.steps_index = 0 self.ease_factor = STARTING_EASE self.interval = None self.history = [] def __repr__(self): return "Card[%s; steps_idx=%s; ease=%s; interval=%s]" % (self.status, self.steps_index, self.ease_factor, str(self.interval)) def choice(self, button: str): '''button is one of "wrong", "hard", "good", or "easy" returns a result in days''' self.history.append(button) if self.status == 'learning': # for learning cards, there is no "hard" response possible if button == "wrong": self.steps_index = 0 return self.minutes_to_days(NEW_STEPS[self.steps_index]) elif button == "good": self.steps_index += 1 if self.steps_index < len(NEW_STEPS): return self.minutes_to_days(NEW_STEPS[self.steps_index]) else: # we have graduated! self.status = 'learned' self.interval = GRADUATING_INTERVAL return self.interval elif button == "easy": self.status = 'learned' self.interval = EASY_INTERVAL return EASY_INTERVAL else: # raise ValueError("you can't press this button / we don't know how to deal with this case") return elif self.status == 'learned': if button == "wrong": self.status = 'relearning' self.steps_index = 0 self.ease_factor = max(1.30, self.ease_factor - 0.20) # the anki manual says "the current interval is multiplied by the # value of new interval", but I have no idea what the "new # interval" is return self.minutes_to_days(LAPSES_STEPS[0]) elif button == "hard": self.ease_factor = max(1.30, self.ease_factor - 0.15) self.interval = self.interval * 1.2 * INTERVAL_MODIFIER return min(MAXIMUM_INTERVAL, self.interval) elif button == "good": self.interval = (self.interval * self.ease_factor * INTERVAL_MODIFIER) return min(MAXIMUM_INTERVAL, self.interval) elif button == "easy": self.ease_factor += 0.15 self.interval = (self.interval * self.ease_factor * INTERVAL_MODIFIER * EASY_BONUS) return min(MAXIMUM_INTERVAL, self.interval) else: # raise ValueError("you can't press this button / we don't know how to deal with this case") return elif self.status == 'relearning': if button == "wrong": self.steps_index = 0 return self.minutes_to_days(LAPSES_STEPS[0]) elif button == "good": self.steps_index += 1 if self.steps_index < len(LAPSES_STEPS): return self.minutes_to_days(LAPSES_STEPS[self.steps_index]) else: # we have re-graduated! self.status = 'learned' self.interval = max(MINIMUM_INTERVAL, self.interval * NEW_INTERVAL) return self.interval else: # raise ValueError("you can't press this button / we don't know how to deal with this case") return def minutes_to_days(self, minutes): return minutes / (60 * 24) @property def prompt(self): c = Card() wrong_ivl = [c.choice(b) for b in self.history + ["wrong"]][-1] c = Card() hard_ivl = [c.choice(b) for b in self.history + ["hard"]][-1] c = Card() good_ivl = [c.choice(b) for b in self.history + ["good"]][-1] c = Card() easy_ivl = [c.choice(b) for b in self.history + ["easy"]][-1] def prompt_pp(ivl, s: str): if ivl: if ivl <= 1: return f'{s} {ivl * 1440}m' else: return f'{s} {round(ivl, 2)}d' s = " | ".join(filter(None, [ prompt_pp(wrong_ivl, "wrong"), prompt_pp(hard_ivl, "hard"), prompt_pp(good_ivl, "good"), prompt_pp(easy_ivl, "easy") ])) return s # python # >>> from anki_algorithm import * # >>> a = Card() # >>> a.prompt # >>> a.choice("good") # >>> a.choice("good") # >>> a.prompt