Skip to main content

eulumdat_quiz/
session.rs

1use crate::{
2    AnswerResult, Category, CategoryScore, Difficulty, DifficultyScore, Question, QuizConfig,
3    QuizScore,
4};
5
6/// A quiz session that tracks progress and scoring.
7#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
8pub struct QuizSession {
9    pub questions: Vec<Question>,
10    pub answers: Vec<Option<u8>>,
11    pub current_index: usize,
12}
13
14impl QuizSession {
15    /// Create a new session from config.
16    pub fn new(config: QuizConfig) -> Self {
17        let all = crate::questions::all_questions();
18
19        let mut filtered: Vec<Question> = all
20            .into_iter()
21            .filter(|q| {
22                if !config.categories.is_empty() && !config.categories.contains(&q.category) {
23                    return false;
24                }
25                if let Some(ref diff) = config.difficulty {
26                    if q.difficulty != *diff {
27                        return false;
28                    }
29                }
30                true
31            })
32            .collect();
33
34        if config.shuffle {
35            // Simple seeded shuffle (xorshift64)
36            let mut rng = config.seed.unwrap_or(0x517cc1b727220a95);
37            for i in (1..filtered.len()).rev() {
38                rng ^= rng << 13;
39                rng ^= rng >> 7;
40                rng ^= rng << 17;
41                let j = (rng as usize) % (i + 1);
42                filtered.swap(i, j);
43            }
44        }
45
46        if config.num_questions > 0 {
47            filtered.truncate(config.num_questions as usize);
48        }
49
50        let len = filtered.len();
51        Self {
52            questions: filtered,
53            answers: vec![None; len],
54            current_index: 0,
55        }
56    }
57
58    /// Get the current question (None if finished).
59    pub fn current_question(&self) -> Option<Question> {
60        self.questions.get(self.current_index).cloned()
61    }
62
63    /// Submit answer for current question, advance to next.
64    pub fn answer(&mut self, choice: u8) -> AnswerResult {
65        let q = &self.questions[self.current_index];
66        let is_correct = choice == q.correct_index;
67        let result = AnswerResult {
68            is_correct,
69            correct_index: q.correct_index,
70            explanation: q.explanation.clone(),
71            reference: q.reference.clone(),
72        };
73        self.answers[self.current_index] = Some(choice);
74        self.current_index += 1;
75        result
76    }
77
78    /// Skip current question, advance to next. Returns false if already finished.
79    pub fn skip(&mut self) -> bool {
80        if self.is_finished() {
81            return false;
82        }
83        // answers[current_index] stays None = skipped
84        self.current_index += 1;
85        true
86    }
87
88    /// Is the quiz finished?
89    pub fn is_finished(&self) -> bool {
90        self.current_index >= self.questions.len()
91    }
92
93    /// Current progress (0-based index, total).
94    pub fn progress(&self) -> (usize, usize) {
95        (self.current_index, self.questions.len())
96    }
97
98    /// Current score (updates live).
99    pub fn score(&self) -> QuizScore {
100        let mut correct = 0u32;
101        let mut wrong = 0u32;
102        let mut skipped = 0u32;
103
104        // Count by category
105        let mut cat_correct: std::collections::HashMap<Category, u32> =
106            std::collections::HashMap::new();
107        let mut cat_total: std::collections::HashMap<Category, u32> =
108            std::collections::HashMap::new();
109
110        // Count by difficulty
111        let mut diff_correct: std::collections::HashMap<Difficulty, u32> =
112            std::collections::HashMap::new();
113        let mut diff_total: std::collections::HashMap<Difficulty, u32> =
114            std::collections::HashMap::new();
115
116        for (i, q) in self.questions.iter().enumerate() {
117            if i >= self.current_index {
118                break;
119            }
120            *cat_total.entry(q.category).or_default() += 1;
121            *diff_total.entry(q.difficulty).or_default() += 1;
122
123            match self.answers[i] {
124                Some(choice) => {
125                    if choice == q.correct_index {
126                        correct += 1;
127                        *cat_correct.entry(q.category).or_default() += 1;
128                        *diff_correct.entry(q.difficulty).or_default() += 1;
129                    } else {
130                        wrong += 1;
131                    }
132                }
133                None => {
134                    skipped += 1;
135                }
136            }
137        }
138
139        let by_category: Vec<CategoryScore> = cat_total
140            .into_iter()
141            .map(|(cat, total)| CategoryScore {
142                correct: *cat_correct.get(&cat).unwrap_or(&0),
143                category: cat,
144                total,
145            })
146            .collect();
147
148        let by_difficulty: Vec<DifficultyScore> = diff_total
149            .into_iter()
150            .map(|(diff, total)| DifficultyScore {
151                correct: *diff_correct.get(&diff).unwrap_or(&0),
152                difficulty: diff,
153                total,
154            })
155            .collect();
156
157        QuizScore {
158            correct,
159            wrong,
160            skipped,
161            total: self.questions.len() as u32,
162            by_category,
163            by_difficulty,
164        }
165    }
166}