konnektoren_core/achievements/
game_statistics.rs

1use super::achievement_statistic::*;
2use crate::analytics::Metric;
3use crate::challenges::performance::Performance;
4use crate::game::{Game, GamePath};
5use std::collections::HashSet;
6
7pub struct GameStatistics<'a> {
8    game: &'a Game,
9}
10
11impl<'a> GameStatistics<'a> {
12    pub fn new(game: &'a Game) -> Self {
13        GameStatistics { game }
14    }
15}
16
17impl TotalChallenges for GameStatistics<'_> {
18    fn total_challenges(&self) -> u32 {
19        self.game.challenge_history.len() as u32
20    }
21}
22
23impl AveragePerformance for GameStatistics<'_> {
24    fn average_performance(&self) -> f64 {
25        if self.game.challenge_history.is_empty() {
26            return 0.0;
27        }
28        let total_performance: u32 = self
29            .game
30            .challenge_history
31            .challenges
32            .iter()
33            .map(|challenge| challenge.performance(&challenge.challenge_result))
34            .sum();
35        total_performance as f64 / self.game.challenge_history.len() as f64
36    }
37}
38
39impl TotalXp for GameStatistics<'_> {
40    fn total_xp(&self) -> u32 {
41        self.game
42            .challenge_history
43            .challenges
44            .iter()
45            .map(|challenge| self.game.calculate_xp_reward(challenge))
46            .sum()
47    }
48}
49
50impl CompletedGamePaths for GameStatistics<'_> {
51    fn completed_game_paths(&self) -> u32 {
52        fn is_game_path_completed(game: &Game, path: &GamePath) -> bool {
53            path.challenges.iter().all(|challenge_config| {
54                game.challenge_history
55                    .challenges
56                    .iter()
57                    .any(|completed_challenge| {
58                        completed_challenge.challenge_config.id == challenge_config.id
59                    })
60            })
61        }
62
63        self.game
64            .game_paths
65            .iter()
66            .filter(|path| is_game_path_completed(self.game, path))
67            .count() as u32
68    }
69}
70
71impl PerfectChallenges for GameStatistics<'_> {
72    fn perfect_challenges(&self) -> u32 {
73        self.game
74            .challenge_history
75            .challenges
76            .iter()
77            .filter(|challenge| challenge.performance(&challenge.challenge_result) == 100)
78            .count() as u32
79    }
80}
81
82impl DifferentChallengeTypesCompleted for GameStatistics<'_> {
83    fn different_challenge_types_completed(&self) -> u32 {
84        let unique_types: HashSet<_> = self
85            .game
86            .challenge_history
87            .challenges
88            .iter()
89            .map(|challenge| challenge.challenge_type.id())
90            .collect();
91        unique_types.len() as u32
92    }
93}
94
95// Implement Metric for GameStatistics
96impl Metric for GameStatistics<'_> {
97    fn name(&self) -> &str {
98        "game_statistics"
99    }
100
101    fn value(&self) -> f64 {
102        0.0
103    }
104
105    fn description(&self) -> &str {
106        "Overall game statistics and metrics"
107    }
108}
109
110// Implement AchievementStatistic for GameStatistics
111impl AchievementStatistic for GameStatistics<'_> {}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::challenges::sort_table::SortTableColumn;
117    use crate::challenges::{
118        Challenge, ChallengeConfig, ChallengeResult, ChallengeType, Choice, ContextItem,
119        ContextItemChoiceAnswers, ContextualChoice, MultipleChoice, MultipleChoiceOption,
120        SortTable, SortTableRow,
121    };
122    use crate::game::Game;
123    use crate::prelude::Question;
124
125    fn create_mock_game(num_challenges: usize, _performance: u32) -> Game {
126        let mut game = Game::default();
127        for i in 0..num_challenges {
128            let (challenge_type, challenge_result) = match i % 3 {
129                0 => (
130                    ChallengeType::MultipleChoice(MultipleChoice {
131                        id: "mc".to_string(),
132                        questions: vec![Question::default()],
133                        options: vec![MultipleChoiceOption::default()],
134                        ..Default::default()
135                    }),
136                    ChallengeResult::MultipleChoice(vec![MultipleChoiceOption::default()]),
137                ),
138                1 => (
139                    ChallengeType::ContextualChoice(ContextualChoice {
140                        id: "cc".to_string(),
141                        items: vec![ContextItem {
142                            template: "".to_string(),
143                            choices: vec![Choice {
144                                id: 0,
145                                options: vec!["".to_string()],
146                                correct_answer: "".to_string(),
147                            }],
148                        }],
149                        ..Default::default()
150                    }),
151                    ChallengeResult::ContextualChoice(vec![ContextItemChoiceAnswers::default()]),
152                ),
153                _ => (
154                    ChallengeType::SortTable(SortTable {
155                        id: "st".to_string(),
156                        name: "".to_string(),
157                        description: "".to_string(),
158                        columns: vec![SortTableColumn {
159                            id: "".to_string(),
160                            title: "".to_string(),
161                            description: "".to_string(),
162                        }],
163                        rows: vec![SortTableRow {
164                            id: 0,
165                            values: vec!["".to_string()],
166                        }],
167                    }),
168                    ChallengeResult::SortTable(vec![SortTableRow::default()]),
169                ),
170            };
171
172            let mut challenge = Challenge::new(
173                &challenge_type,
174                &ChallengeConfig {
175                    id: format!("challenge_{}", i),
176                    ..Default::default()
177                },
178            );
179            challenge.challenge_result = challenge_result;
180            game.challenge_history.add_challenge(challenge);
181        }
182
183        // Set performance for each challenge (this is a simplified version)
184        for challenge in &mut game.challenge_history.challenges {
185            match &mut challenge.challenge_result {
186                ChallengeResult::MultipleChoice(options) => {
187                    options[0].id = 0;
188                }
189                ChallengeResult::ContextualChoice(choices) => {
190                    choices[0].ids = vec![0];
191                }
192                ChallengeResult::SortTable(rows) => {
193                    rows[0].values = vec!["".to_string()];
194                }
195                _ => {}
196            }
197        }
198
199        game
200    }
201
202    #[test]
203    fn test_total_challenges() {
204        let game = create_mock_game(5, 80);
205        let stats = GameStatistics::new(&game);
206        assert_eq!(stats.total_challenges(), 5);
207    }
208
209    #[test]
210    fn test_average_performance() {
211        let game = create_mock_game(3, 100);
212        let stats = GameStatistics::new(&game);
213        assert_eq!(stats.average_performance(), 100.0);
214    }
215
216    #[test]
217    fn test_total_xp() {
218        let game = create_mock_game(2, 100);
219        let stats = GameStatistics::new(&game);
220        assert_eq!(stats.total_xp(), 600);
221    }
222
223    #[test]
224    fn test_perfect_challenges() {
225        let game = create_mock_game(5, 100);
226        let stats = GameStatistics::new(&game);
227        assert_eq!(stats.perfect_challenges(), 5);
228    }
229
230    #[test]
231    fn test_different_challenge_types_completed() {
232        let game = create_mock_game(3, 80);
233
234        let stats = GameStatistics::new(&game);
235        assert_eq!(stats.different_challenge_types_completed(), 3);
236    }
237}