konnektoren_core/achievements/
game_statistics.rs1use 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
95impl 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
110impl 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 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}