1use crate::error::{GameError, GameResult};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct GameSessionStats {
10 pub session_id: u64,
12 pub final_score: u32,
14 pub moves: u32,
16 pub duration: u64,
18 pub max_tile: u32,
20 pub won: bool,
22 pub end_reason: GameEndReason,
24 pub start_time: u64,
26 pub end_time: u64,
28 pub avg_score_per_move: f64,
30 pub efficiency: f64,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub enum GameEndReason {
37 Won,
39 GameOver,
41 Abandoned,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct StatisticsSummary {
48 pub total_games: u32,
50 pub games_won: u32,
52 pub win_rate: f64,
54 pub highest_score: u32,
56 pub average_score: f64,
58 pub total_moves: u32,
60 pub average_moves: f64,
62 pub total_play_time: u64,
64 pub average_duration: f64,
66 pub highest_tile: u32,
68 pub tile_distribution: HashMap<u32, u32>,
70 pub score_distribution: ScoreDistribution,
72 pub recent_games: Vec<GameSessionStats>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, Default)]
78pub struct ScoreDistribution {
79 pub low_score: u32,
81 pub medium_score: u32,
83 pub high_score: u32,
85 pub very_high_score: u32,
87}
88
89pub struct StatisticsManager {
91 stats_file: String,
93 sessions: Vec<GameSessionStats>,
95}
96
97impl StatisticsManager {
98 pub fn new(stats_file: &str) -> GameResult<Self> {
100 let mut manager = Self {
101 stats_file: stats_file.to_string(),
102 sessions: Vec::new(),
103 };
104
105 manager.load_statistics()?;
107
108 Ok(manager)
109 }
110
111 pub fn record_session(&mut self, session: GameSessionStats) -> GameResult<()> {
113 self.sessions.push(session);
114 self.save_statistics()?;
115 Ok(())
116 }
117
118 pub fn get_summary(&self) -> StatisticsSummary {
120 if self.sessions.is_empty() {
121 return StatisticsSummary {
122 total_games: 0,
123 games_won: 0,
124 win_rate: 0.0,
125 highest_score: 0,
126 average_score: 0.0,
127 total_moves: 0,
128 average_moves: 0.0,
129 total_play_time: 0,
130 average_duration: 0.0,
131 highest_tile: 0,
132 tile_distribution: HashMap::new(),
133 score_distribution: ScoreDistribution::default(),
134 recent_games: Vec::new(),
135 };
136 }
137
138 let total_games = self.sessions.len() as u32;
139 let games_won = self.sessions.iter().filter(|s| s.won).count() as u32;
140 let win_rate = (games_won as f64 / total_games as f64) * 100.0;
141
142 let highest_score = self
143 .sessions
144 .iter()
145 .map(|s| s.final_score)
146 .max()
147 .unwrap_or(0);
148 let average_score = self
149 .sessions
150 .iter()
151 .map(|s| s.final_score as f64)
152 .sum::<f64>()
153 / total_games as f64;
154
155 let total_moves = self.sessions.iter().map(|s| s.moves).sum::<u32>();
156 let average_moves = total_moves as f64 / total_games as f64;
157
158 let total_play_time = self.sessions.iter().map(|s| s.duration).sum::<u64>();
159 let average_duration = total_play_time as f64 / total_games as f64;
160
161 let highest_tile = self.sessions.iter().map(|s| s.max_tile).max().unwrap_or(0);
162
163 let mut tile_distribution = HashMap::new();
165 for session in &self.sessions {
166 *tile_distribution.entry(session.max_tile).or_insert(0) += 1;
167 }
168
169 let mut score_distribution = ScoreDistribution::default();
171 for session in &self.sessions {
172 match session.final_score {
173 0..=1000 => score_distribution.low_score += 1,
174 1001..=5000 => score_distribution.medium_score += 1,
175 5001..=10000 => score_distribution.high_score += 1,
176 _ => score_distribution.very_high_score += 1,
177 }
178 }
179
180 let mut recent_games = self.sessions.clone();
182 recent_games.sort_by(|a, b| b.end_time.cmp(&a.end_time));
183 recent_games.truncate(10);
184
185 StatisticsSummary {
186 total_games,
187 games_won,
188 win_rate,
189 highest_score,
190 average_score,
191 total_moves,
192 average_moves,
193 total_play_time,
194 average_duration,
195 highest_tile,
196 tile_distribution,
197 score_distribution,
198 recent_games,
199 }
200 }
201
202 pub fn get_score_trend(&self, count: usize) -> Vec<(u32, u32)> {
204 let mut recent_sessions = self.sessions.clone();
205 recent_sessions.sort_by(|a, b| b.end_time.cmp(&a.end_time));
206 recent_sessions.truncate(count);
207 recent_sessions.reverse();
208
209 recent_sessions
210 .iter()
211 .enumerate()
212 .map(|(i, session)| (i as u32, session.final_score))
213 .collect()
214 }
215
216 pub fn get_efficiency_trend(&self, count: usize) -> Vec<(u32, f64)> {
218 let mut recent_sessions = self.sessions.clone();
219 recent_sessions.sort_by(|a, b| b.end_time.cmp(&a.end_time));
220 recent_sessions.truncate(count);
221 recent_sessions.reverse();
222
223 recent_sessions
224 .iter()
225 .enumerate()
226 .map(|(i, session)| (i as u32, session.efficiency))
227 .collect()
228 }
229
230 pub fn get_tile_achievements(&self) -> Vec<(u32, u32)> {
232 let mut tile_counts: Vec<(u32, u32)> = self
233 .sessions
234 .iter()
235 .fold(HashMap::new(), |mut acc, session| {
236 *acc.entry(session.max_tile).or_insert(0) += 1;
237 acc
238 })
239 .into_iter()
240 .collect();
241
242 tile_counts.sort_by(|a, b| a.0.cmp(&b.0));
243 tile_counts
244 }
245
246 fn load_statistics(&mut self) -> GameResult<()> {
248 if !Path::new(&self.stats_file).exists() {
249 return Ok(());
250 }
251
252 let content = fs::read_to_string(&self.stats_file).map_err(|e| {
253 GameError::InvalidOperation(format!("Failed to read stats file: {}", e))
254 })?;
255
256 self.sessions = serde_json::from_str(&content).map_err(|e| {
257 GameError::InvalidOperation(format!("Failed to parse stats file: {}", e))
258 })?;
259
260 Ok(())
261 }
262
263 fn save_statistics(&self) -> GameResult<()> {
265 let content = serde_json::to_string_pretty(&self.sessions).map_err(|e| {
266 GameError::InvalidOperation(format!("Failed to serialize stats: {}", e))
267 })?;
268
269 fs::write(&self.stats_file, content).map_err(|e| {
270 GameError::InvalidOperation(format!("Failed to write stats file: {}", e))
271 })?;
272
273 Ok(())
274 }
275
276 pub fn clear_statistics(&mut self) -> GameResult<()> {
278 self.sessions.clear();
279 self.save_statistics()?;
280 Ok(())
281 }
282
283 pub fn export_statistics(&self) -> GameResult<String> {
285 serde_json::to_string_pretty(&self.sessions)
286 .map_err(|e| GameError::InvalidOperation(format!("Failed to export stats: {}", e)))
287 }
288}
289
290pub fn create_session_stats(
292 final_score: u32,
293 moves: u32,
294 duration: u64,
295 max_tile: u32,
296 won: bool,
297 start_time: u64,
298 end_time: u64,
299) -> GameSessionStats {
300 let end_reason = if won {
301 GameEndReason::Won
302 } else {
303 GameEndReason::GameOver
304 };
305
306 let avg_score_per_move = if moves > 0 {
307 final_score as f64 / moves as f64
308 } else {
309 0.0
310 };
311
312 let efficiency = if moves > 0 {
313 final_score as f64 / moves as f64
314 } else {
315 0.0
316 };
317
318 GameSessionStats {
319 session_id: start_time,
320 final_score,
321 moves,
322 duration,
323 max_tile,
324 won,
325 end_reason,
326 start_time,
327 end_time,
328 avg_score_per_move,
329 efficiency,
330 }
331}