rusty2048_core/
stats.rs

1use crate::error::{GameError, GameResult};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7/// Single game session statistics
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct GameSessionStats {
10    /// Game session ID (timestamp)
11    pub session_id: u64,
12    /// Final score
13    pub final_score: u32,
14    /// Number of moves made
15    pub moves: u32,
16    /// Game duration in seconds
17    pub duration: u64,
18    /// Maximum tile achieved
19    pub max_tile: u32,
20    /// Whether the game was won
21    pub won: bool,
22    /// Game end reason
23    pub end_reason: GameEndReason,
24    /// Timestamp when game started
25    pub start_time: u64,
26    /// Timestamp when game ended
27    pub end_time: u64,
28    /// Average score per move
29    pub avg_score_per_move: f64,
30    /// Efficiency score (score / moves)
31    pub efficiency: f64,
32}
33
34/// Game end reason
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub enum GameEndReason {
37    /// Game was won (reached target)
38    Won,
39    /// Game over (no more moves)
40    GameOver,
41    /// Game was abandoned
42    Abandoned,
43}
44
45/// Overall statistics summary
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct StatisticsSummary {
48    /// Total number of games played
49    pub total_games: u32,
50    /// Total number of games won
51    pub games_won: u32,
52    /// Win rate percentage
53    pub win_rate: f64,
54    /// Highest score ever achieved
55    pub highest_score: u32,
56    /// Average score across all games
57    pub average_score: f64,
58    /// Total moves across all games
59    pub total_moves: u32,
60    /// Average moves per game
61    pub average_moves: f64,
62    /// Total play time in seconds
63    pub total_play_time: u64,
64    /// Average game duration
65    pub average_duration: f64,
66    /// Highest tile ever achieved
67    pub highest_tile: u32,
68    /// Tile distribution (how many times each tile was achieved)
69    pub tile_distribution: HashMap<u32, u32>,
70    /// Score distribution (ranges)
71    pub score_distribution: ScoreDistribution,
72    /// Recent games (last 10)
73    pub recent_games: Vec<GameSessionStats>,
74}
75
76/// Score distribution by ranges
77#[derive(Debug, Clone, Serialize, Deserialize, Default)]
78pub struct ScoreDistribution {
79    /// Games with score 0-1000
80    pub low_score: u32,
81    /// Games with score 1001-5000
82    pub medium_score: u32,
83    /// Games with score 5001-10000
84    pub high_score: u32,
85    /// Games with score 10001+
86    pub very_high_score: u32,
87}
88
89/// Statistics manager for tracking and analyzing game data
90pub struct StatisticsManager {
91    /// Path to statistics file
92    stats_file: String,
93    /// All game sessions
94    sessions: Vec<GameSessionStats>,
95}
96
97impl StatisticsManager {
98    /// Create a new statistics manager
99    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        // Load existing statistics
106        manager.load_statistics()?;
107
108        Ok(manager)
109    }
110
111    /// Record a new game session
112    pub fn record_session(&mut self, session: GameSessionStats) -> GameResult<()> {
113        self.sessions.push(session);
114        self.save_statistics()?;
115        Ok(())
116    }
117
118    /// Get statistics summary
119    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        // Calculate tile distribution
164        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        // Calculate score distribution
170        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        // Get recent games (last 10)
181        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    /// Get score trend data (last N games)
203    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    /// Get efficiency trend data (last N games)
217    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    /// Get tile achievement data
231    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    /// Load statistics from file
247    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    /// Save statistics to file
264    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    /// Clear all statistics
277    pub fn clear_statistics(&mut self) -> GameResult<()> {
278        self.sessions.clear();
279        self.save_statistics()?;
280        Ok(())
281    }
282
283    /// Export statistics to JSON
284    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
290/// Helper function to create a game session from game stats
291pub 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}