Files
jap_learn/src/core/srs.rs
T

188 lines
5.1 KiB
Rust
Raw Normal View History

2026-08-24 11:40:31 +03:00
use crate::core::deck::DeckData;
2026-08-24 01:37:00 +03:00
use rand::distr::weighted::WeightedIndex;
use rand::prelude::ThreadRng;
use rand::prelude::{Distribution, SliceRandom};
use rand::rng;
2026-08-24 11:40:31 +03:00
use crate::data::entities::CardStatistics;
2026-08-24 01:37:00 +03:00
const MAX_HISTORY_LEN: usize = 20;
const MAX_HISTORY_LEN_PART: f32 = 0.33;
pub const MAX_SCORE: u8 = 25;
pub const FADE_PER_DAY: f32 = 0.95;
#[derive(Clone, PartialEq, Copy, Eq)]
pub enum OrderMode {
Default,
TrainWorstFirst,
FullRandom,
}
#[derive(Clone, Copy, Eq, PartialEq, Default)]
pub enum AppendMode {
Full,
#[default]
Manual,
}
#[derive(Clone)]
pub enum OrderModule {
SemiRandomSRS(SemiRandomSRSModule),
RandomSRS(RandomSRSModule),
WorstWordsSRS(WorstWordsSRSModule),
}
pub trait SRSModule {
fn next(&mut self, set: &mut DeckData) -> usize;
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &CardStatistics);
fn init(&mut self, set: &mut DeckData);
}
#[derive(Clone)]
pub struct RandomSRSModule {
basket: Vec<usize>,
pub(crate) initialized: bool,
}
impl RandomSRSModule {
pub(crate) fn new() -> RandomSRSModule {
Self {
basket: vec![],
initialized: false,
}
}
}
impl SRSModule for RandomSRSModule {
fn next(&mut self, set: &mut DeckData) -> usize {
match self.basket.pop() {
Some(index) => index,
None => {
self.basket = (0..set.words.len()).collect::<Vec<usize>>();
self.basket.shuffle(&mut rand::rng());
self.basket.pop().expect("empty basket")
}
}
}
fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {}
fn init(&mut self, set: &mut DeckData) {
self.initialized = true;
self.basket = (0..set.set.len()).collect::<Vec<usize>>();
self.basket.shuffle(&mut rand::rng())
}
}
#[derive(Clone)]
pub struct SemiRandomSRSModule {
history: Vec<usize>,
last_weights: WeightedIndex<f32>,
generator: ThreadRng,
pub(crate) initialized: bool,
}
impl SemiRandomSRSModule {
pub(crate) fn new() -> SemiRandomSRSModule {
SemiRandomSRSModule {
history: vec![],
last_weights: WeightedIndex::new([1.0]).unwrap(),
generator: rng(),
initialized: false,
}
}
}
impl SRSModule for SemiRandomSRSModule {
fn next(&mut self, set: &mut DeckData) -> usize {
let index = self.last_weights.sample(&mut self.generator);
if self.history.contains(&index) && self.history.len() != 1 {
return self.next(set);
}
if self.history.len() == self.history_len(set) {
self.history.remove(0);
}
self.history.push(index);
index
}
fn open(&mut self, _: WordOpenMode, index: usize, word: &CardStatistics) {
let new_weight = (100.0 / word.calculated_score()).powf(2.0);
self.last_weights
.update_weights(&[(index, &new_weight)])
.expect("update weights error");
}
fn init(&mut self, set: &mut DeckData) {
self.initialized = true;
let weights = set
.set
.iter()
.map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0)
.collect::<Vec<f32>>();
self.last_weights = WeightedIndex::new(weights).unwrap();
}
}
impl SemiRandomSRSModule {
fn history_len(&self, set: &DeckData) -> usize {
((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN)
}
}
#[derive(Clone)]
pub struct WorstWordsSRSModule {
pub(crate) initialized: bool,
pool_size: usize,
rounds_remaining: u8,
pool: Vec<usize>,
queue: Vec<usize>,
rounds_count: u8,
}
impl SRSModule for WorstWordsSRSModule {
fn next(&mut self, set: &mut DeckData) -> usize {
if self.rounds_remaining == 0 {
self.fill_pool(set);
self.rounds_remaining = self.rounds_count;
}
match self.queue.pop() {
Some(index) => index,
None => {
self.queue.append(&mut self.pool.clone());
self.rounds_remaining -= 1;
self.queue.pop().expect("empty queue")
}
}
}
fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {}
fn init(&mut self, _: &mut DeckData) {
self.initialized = true;
}
}
impl WorstWordsSRSModule {
pub(crate) fn new() -> WorstWordsSRSModule {
WorstWordsSRSModule {
initialized: false,
pool_size: 15,
rounds_count: 2,
rounds_remaining: 0,
pool: vec![],
queue: vec![],
}
}
fn fill_pool(&mut self, set: &DeckData) {
let mut sorted = set
.set
.clone()
.into_iter()
.zip(0..set.set.len())
.collect::<Vec<_>>();
sorted.sort_by_key(|c| c.0.score);
let mut worst = sorted[0..self.pool_size]
.iter()
.map(|(_, index)| *index)
.collect::<Vec<usize>>();
worst.shuffle(&mut rand::rng());
self.pool = worst;
}
}
#[derive(Clone, Copy)]
pub enum WordOpenMode {
Easy,
Ok,
Hard,
None,
2026-08-24 11:40:31 +03:00
}