janki/
game.rs

1use crate::{
2    dummy_storage::{DummyStorage, DynStorage},
3    item::{Fact, Item, ItemGuard},
4    storage::Storage,
5};
6use rand::{thread_rng, Rng};
7use std::{
8    collections::HashMap,
9    marker::PhantomData,
10    time::{Duration, SystemTime},
11};
12
13///Alias used to determine how long the space is between repetitions based on the current streak
14pub type SeeAgainGaps = HashMap<u32, Duration>;
15///Utility alias for a Vec<Item>
16pub type AnkiDB = Vec<Item>;
17
18///muah ha ha ha ha
19///
20///Library users can't see this, so I can do real messed up stuff
21mod private_trait {
22    ///get recked lol
23    pub trait CannotExternallyImplement {}
24}
25
26///Trait for clients to decide how they want to get the facts. Shenanigans have occured so that clients **cannot** implement this
27pub trait AnkiCardReturnType: private_trait::CannotExternallyImplement {}
28///Marker Struct that implements [`AnkiCardType`] where the client receives an [`ItemGuard`]
29pub struct GiveItemGuards;
30///Marker Struct that implements [`AnkiCardType`] where the client receives a [`Fact`]
31pub struct GiveFacts;
32impl private_trait::CannotExternallyImplement for GiveItemGuards {}
33impl private_trait::CannotExternallyImplement for GiveFacts {}
34impl AnkiCardReturnType for GiveItemGuards {}
35impl AnkiCardReturnType for GiveFacts {}
36
37///Provides a default [`SeeAgainGaps`] - useful for testing
38#[must_use]
39pub fn default_sag() -> SeeAgainGaps {
40    let mut hm = HashMap::new();
41    for i in 1..11 {
42        hm.insert(i, Duration::from_secs(u64::from(i) * 30));
43    }
44    hm
45}
46
47///Struct used to manage the game - this should be used in the client
48pub struct AnkiGame<S: Storage, T: AnkiCardReturnType> {
49    ///Vector to store the items
50    v: AnkiDB,
51    ///Storage for the AnkiDB
52    pub(crate) storage: S,
53    ///Timer for spaced repetition
54    sag: SeeAgainGaps,
55    ///Stores the index of the card being tested if [`AnkiCardType`] == [`GiveMeFacts`]
56    current: Option<(usize, bool)>,
57
58    ///Makes sure that the [`AnkiCardType`] isn't optimised away
59    _pd: PhantomData<T>,
60}
61
62impl<S: Storage, T: AnkiCardReturnType> AnkiGame<S, T> {
63    ///Constructor function - sets all fields to arguments, and uses the [`Storage`] to read the database.
64    ///
65    ///Can return [`Result::Err`] if there is an error reading the database
66    pub fn new(storage: S, sat: SeeAgainGaps) -> Result<Self, S::ErrorType> {
67        Ok(Self {
68            v: storage.read_db()?,
69            storage,
70            sag: sat,
71            current: None,
72            _pd: PhantomData,
73        })
74    }
75
76    ///Adds a new item to the [`AnkiDB`] using [`Into::into`] - which sets the streak to 0, and the last tested to [`Option::None`]
77    pub fn add_card(&mut self, f: Fact) {
78        self.v.push(f.into());
79        self.storage.write_db(&self.v).unwrap();
80    }
81
82    ///Gets all the current eligible facts - the ordering is **not** related to anything
83    #[must_use]
84    pub fn get_elgible(&self) -> Vec<Fact> {
85        let indicies = get_eligible(&self.v, &self.sag);
86        indicies
87            .into_iter()
88            .map(|index| &self.v[index].fact)
89            .cloned()
90            .collect()
91    }
92
93    ///Get the number of facts in the eligible list
94    #[must_use]
95    pub fn get_eligible_no(&self) -> usize {
96        get_eligible(&self.v, &self.sag).len()
97    }
98
99    ///Gets **all** of the current acts, ordering useless
100    #[must_use]
101    pub fn get_all_facts(&self) -> Vec<Fact> {
102        self.v.clone().into_iter().map(|i| i.fact).collect()
103    }
104
105    ///Writes to the database - useful if the function is called externally, like in eframe
106    pub fn write_to_db(&mut self) -> Result<(), S::ErrorType> {
107        self.storage.write_db(&self.v)
108    }
109
110    ///Gets an index for use in a [`get_new_card`] or [`get_fact`]
111    ///
112    ///Returns the index to use and a bool for whether the item was taken from the eligible list
113    fn get_an_index(&self) -> Option<(usize, bool)> {
114        let eligible = get_eligible(&self.v, &self.sag);
115
116        if eligible.is_empty() {
117            if self.v.is_empty() {
118                None
119            } else {
120                Some((thread_rng().gen_range(0..self.v.len()), false))
121            }
122        } else {
123            Some((eligible[thread_rng().gen_range(0..eligible.len())], true))
124        }
125    }
126}
127
128impl<S: Storage> AnkiGame<S, GiveItemGuards> {
129    ///Gets a new card from the eligible list. If there are no terms, it will return [`Option::None`].
130    ///
131    ///Returns an [`ItemGuard`] and a [`bool`] for whether the item was taken from the eligible list
132    pub fn get_item_guard(&mut self) -> Option<(ItemGuard<S>, bool)> {
133        if let Some((index, was_e)) = self.get_an_index() {
134            Some((ItemGuard::new(&mut self.v, index, &mut self.storage), was_e))
135        } else {
136            None
137        }
138    }
139}
140
141impl<S: Storage> AnkiGame<S, GiveFacts> {
142    ///Gets a fact.
143    ///
144    ///If no facts, will return [`Option::None`], else will return a [`Fact`] and a [`bool`] for whether or not is was from the eligible list
145    pub fn get_fact(&mut self) -> Option<(Fact, bool)> {
146        if let Some((cu, was_e)) = self.current {
147            Some((self.v[cu].fact.clone(), was_e))
148        } else if self.v.is_empty() {
149            None
150        } else {
151            self.set_new_fact();
152            self.get_fact()
153        }
154    }
155
156    ///Combination of [`set_new_fact`] and [`get_fact`] - to ensure that the fact received is new
157    pub fn get_new_fact(&mut self) -> Option<(Fact, bool)> {
158        self.set_new_fact();
159        self.get_fact()
160    }
161
162    ///Sets the current fact to a new fact
163    pub fn set_new_fact(&mut self) {
164        if let Some((index, we)) = self.get_an_index() {
165            self.current = Some((index, we));
166        }
167    }
168
169    ///Signifies that the client is done with the fact.
170    pub fn finish_current_fact(&mut self, correct: bool) {
171        if let Some((cu, _)) = self.current {
172            self.v[cu].history.push(correct);
173            self.v[cu].last_tested = Some(SystemTime::now());
174            self.storage
175                .write_db(&self.v)
176                .expect("unable to write to db");
177        }
178
179        self.current = None;
180    }
181}
182
183impl<E: std::fmt::Debug, T: AnkiCardReturnType> DynStorage<E> for AnkiGame<DummyStorage, T> {
184    fn read_custom(&mut self, s: &dyn Storage<ErrorType = E>) -> Result<(), E> {
185        self.v = s.read_db()?;
186        Ok(())
187    }
188
189    fn write_custom(&mut self, s: &mut dyn Storage<ErrorType = E>) -> Result<(), E> {
190        s.write_db(&self.v)
191    }
192
193    fn exit_custom(&mut self, s: &mut dyn Storage<ErrorType = E>) {
194        s.exit_application();
195    }
196}
197
198///A function to get all of the indexes that need to be tested from a list using a [`SeeAgainGaps`]
199#[must_use]
200pub fn get_eligible(items: &[Item], sag: &SeeAgainGaps) -> Vec<usize> {
201    items
202        .iter()
203        .enumerate()
204        .filter_map(|(index, item)| {
205            item.time_since_last_test()
206                .map_or(Some(index), |last_seen| {
207                    sag.get(&item.get_streak()).map_or(Some(index), |distance| {
208                        if &last_seen > distance {
209                            Some(index)
210                        } else {
211                            None
212                        }
213                    })
214                })
215        })
216        .collect()
217}