Skip to content

Commit 9425bac

Browse files
Merge pull request #35 from unhappychoice/feat/session-summary
feat: Add session summary screen with comprehensive statistics
2 parents a297502 + 0cf2a44 commit 9425bac

File tree

8 files changed

+408
-9
lines changed

8 files changed

+408
-9
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ serde_json = "1.0"
3838
ctrlc = "3.4.7"
3939
uuid = { version = "1.0", features = ["v4"] }
4040
rand = { version = "0.8", features = ["std_rng"] }
41+
once_cell = "1.19"
4142

4243
[dev-dependencies]
4344
tempfile = "3.8"

src/game/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ pub mod ascii_digits;
1010
pub mod ascii_rank_titles_generated;
1111
pub mod typing_animation;
1212
pub mod rank_messages;
13+
pub mod session_tracker;
1314

1415
pub use screens::TypingScreen;
1516
pub use challenge::Challenge;
1617
pub use stage_manager::StageManager;
17-
pub use stage_builder::{StageBuilder, GameMode, DifficultyLevel, StageConfig};
18+
pub use stage_builder::{StageBuilder, GameMode, DifficultyLevel, StageConfig};
19+
pub use session_tracker::{SessionTracker, SessionSummary};
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
use crate::Result;
2+
use crate::game::{SessionSummary, ascii_digits::get_digit_patterns};
3+
use crossterm::{
4+
cursor::MoveTo,
5+
event::{self, Event},
6+
execute,
7+
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
8+
terminal::{self, ClearType},
9+
};
10+
use std::io::{stdout, Write};
11+
12+
#[derive(Debug)]
13+
pub enum ExitAction {
14+
Exit,
15+
}
16+
17+
pub struct ExitSummaryScreen;
18+
19+
impl ExitSummaryScreen {
20+
fn create_ascii_numbers(score: &str) -> Vec<String> {
21+
let digit_patterns = get_digit_patterns();
22+
let max_height = 4;
23+
let mut result = vec![String::new(); max_height];
24+
25+
for ch in score.chars() {
26+
if let Some(digit) = ch.to_digit(10) {
27+
let pattern = &digit_patterns[digit as usize];
28+
for (i, line) in pattern.iter().enumerate() {
29+
result[i].push_str(line);
30+
result[i].push(' ');
31+
}
32+
}
33+
}
34+
35+
result
36+
}
37+
38+
pub fn show(session_summary: &SessionSummary) -> Result<ExitAction> {
39+
let mut stdout = stdout();
40+
execute!(stdout, terminal::Clear(ClearType::All))?;
41+
42+
let (terminal_width, terminal_height) = terminal::size()?;
43+
let center_row = terminal_height / 2;
44+
let center_col = terminal_width / 2;
45+
46+
let title = "🎮 SESSION SUMMARY 🎮";
47+
let title_col = center_col.saturating_sub(title.len() as u16 / 2);
48+
execute!(stdout, MoveTo(title_col, center_row.saturating_sub(12)))?;
49+
execute!(stdout, SetAttribute(Attribute::Bold), SetForegroundColor(Color::Yellow))?;
50+
execute!(stdout, Print(&title))?;
51+
execute!(stdout, ResetColor)?;
52+
53+
// Show session duration
54+
let duration_text = format!("Session Duration: {:.1} minutes", session_summary.total_session_time.as_secs_f64() / 60.0);
55+
let duration_col = center_col.saturating_sub(duration_text.len() as u16 / 2);
56+
execute!(stdout, MoveTo(duration_col, center_row.saturating_sub(10)))?;
57+
execute!(stdout, SetForegroundColor(Color::Cyan))?;
58+
execute!(stdout, Print(&duration_text))?;
59+
execute!(stdout, ResetColor)?;
60+
61+
// Show completion status
62+
let completion_status = session_summary.get_session_completion_status();
63+
let status_col = center_col.saturating_sub(completion_status.len() as u16 / 2);
64+
execute!(stdout, MoveTo(status_col, center_row.saturating_sub(9)))?;
65+
execute!(stdout, SetForegroundColor(Color::Green))?;
66+
execute!(stdout, Print(&completion_status))?;
67+
execute!(stdout, ResetColor)?;
68+
69+
// Show total score as large ASCII
70+
let score_label = "TOTAL SESSION SCORE";
71+
let score_label_col = center_col.saturating_sub(score_label.len() as u16 / 2);
72+
execute!(stdout, MoveTo(score_label_col, center_row.saturating_sub(7)))?;
73+
execute!(stdout, SetAttribute(Attribute::Bold), SetForegroundColor(Color::Cyan))?;
74+
execute!(stdout, Print(score_label))?;
75+
execute!(stdout, ResetColor)?;
76+
77+
let score_value = format!("{:.0}", session_summary.session_score);
78+
let ascii_numbers = Self::create_ascii_numbers(&score_value);
79+
let score_start_row = center_row.saturating_sub(6);
80+
81+
for (row_index, line) in ascii_numbers.iter().enumerate() {
82+
let line_col = center_col.saturating_sub(line.len() as u16 / 2);
83+
execute!(stdout, MoveTo(line_col, score_start_row + row_index as u16))?;
84+
execute!(stdout, SetAttribute(Attribute::Bold), SetForegroundColor(Color::Green))?;
85+
execute!(stdout, Print(line))?;
86+
execute!(stdout, ResetColor)?;
87+
}
88+
89+
// Show session statistics
90+
let stats_start_row = center_row.saturating_sub(1);
91+
92+
let mut stats_lines = vec![
93+
format!("Overall CPM: {:.1} | WPM: {:.1} | Accuracy: {:.1}%",
94+
session_summary.overall_cpm, session_summary.overall_wpm, session_summary.overall_accuracy),
95+
format!("Total Keystrokes: {} | Mistakes: {} | Challenges: {}/{}",
96+
session_summary.total_keystrokes, session_summary.total_mistakes,
97+
session_summary.total_challenges_completed, session_summary.total_challenges_attempted),
98+
];
99+
100+
if session_summary.total_skips_used > 0 {
101+
stats_lines.push(format!("Skips Used: {}", session_summary.total_skips_used));
102+
}
103+
104+
for (i, line) in stats_lines.iter().enumerate() {
105+
let line_col = center_col.saturating_sub(line.len() as u16 / 2);
106+
execute!(stdout, MoveTo(line_col, stats_start_row + i as u16))?;
107+
execute!(stdout, SetForegroundColor(Color::White))?;
108+
execute!(stdout, Print(line))?;
109+
execute!(stdout, ResetColor)?;
110+
}
111+
112+
// Show best/worst performance if we have completed challenges
113+
if session_summary.total_challenges_completed > 0 {
114+
let performance_start_row = stats_start_row + stats_lines.len() as u16 + 1;
115+
116+
let performance_lines = vec![
117+
format!("Best Stage: {:.1} WPM, {:.1}% accuracy",
118+
session_summary.best_stage_wpm, session_summary.best_stage_accuracy),
119+
format!("Worst Stage: {:.1} WPM, {:.1}% accuracy",
120+
session_summary.worst_stage_wpm, session_summary.worst_stage_accuracy),
121+
];
122+
123+
for (i, line) in performance_lines.iter().enumerate() {
124+
let line_col = center_col.saturating_sub(line.len() as u16 / 2);
125+
execute!(stdout, MoveTo(line_col, performance_start_row + i as u16))?;
126+
execute!(stdout, SetForegroundColor(Color::Grey))?;
127+
execute!(stdout, Print(line))?;
128+
execute!(stdout, ResetColor)?;
129+
}
130+
}
131+
132+
133+
// Show exit options
134+
let options_start = if session_summary.total_challenges_completed > 0 {
135+
stats_start_row + stats_lines.len() as u16 + 4
136+
} else {
137+
stats_start_row + stats_lines.len() as u16 + 2
138+
};
139+
// Show thanks message
140+
let thanks_message = "Thanks for playing GitType!";
141+
let thanks_col = center_col.saturating_sub(thanks_message.len() as u16 / 2);
142+
execute!(stdout, MoveTo(thanks_col, options_start))?;
143+
execute!(stdout, SetAttribute(Attribute::Bold), SetForegroundColor(Color::Green))?;
144+
execute!(stdout, Print(thanks_message))?;
145+
execute!(stdout, ResetColor)?;
146+
147+
let options = vec![
148+
"Press any key to exit",
149+
];
150+
151+
for (i, option) in options.iter().enumerate() {
152+
let option_col = center_col.saturating_sub(option.len() as u16 / 2);
153+
execute!(stdout, MoveTo(option_col, options_start + 2 + i as u16))?;
154+
execute!(stdout, SetForegroundColor(Color::Yellow))?;
155+
execute!(stdout, Print(option))?;
156+
execute!(stdout, ResetColor)?;
157+
}
158+
159+
stdout.flush()?;
160+
161+
// Wait for any key press
162+
loop {
163+
if event::poll(std::time::Duration::from_millis(100))? {
164+
if let Event::Key(_) = event::read()? {
165+
return Ok(ExitAction::Exit);
166+
}
167+
}
168+
}
169+
}
170+
}

src/game/screens/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ pub mod countdown_screen;
44
pub mod typing_screen;
55
pub mod loading_screen;
66
pub mod animation_screen;
7+
pub mod exit_summary_screen;
78

89
pub use title_screen::{TitleScreen, TitleAction};
910
pub use result_screen::{ResultScreen, ResultAction};
1011
pub use countdown_screen::CountdownScreen;
1112
pub use typing_screen::TypingScreen;
1213
pub use loading_screen::LoadingScreen;
13-
pub use animation_screen::AnimationScreen;
14+
pub use animation_screen::AnimationScreen;
15+
pub use exit_summary_screen::{ExitSummaryScreen, ExitAction};

src/game/session_tracker.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use std::time::{Duration, Instant};
2+
use crate::scoring::{TypingMetrics, ScoringEngine};
3+
4+
#[derive(Debug, Clone)]
5+
pub struct SessionSummary {
6+
pub session_start_time: Instant,
7+
pub total_session_time: Duration,
8+
pub total_challenges_completed: usize,
9+
pub total_challenges_attempted: usize,
10+
pub total_skips_used: usize,
11+
pub overall_accuracy: f64,
12+
pub overall_wpm: f64,
13+
pub overall_cpm: f64,
14+
pub total_keystrokes: usize,
15+
pub total_mistakes: usize,
16+
pub best_stage_wpm: f64,
17+
pub worst_stage_wpm: f64,
18+
pub best_stage_accuracy: f64,
19+
pub worst_stage_accuracy: f64,
20+
pub session_score: f64,
21+
}
22+
23+
impl SessionSummary {
24+
pub fn new() -> Self {
25+
Self {
26+
session_start_time: Instant::now(),
27+
total_session_time: Duration::default(),
28+
total_challenges_completed: 0,
29+
total_challenges_attempted: 0,
30+
total_skips_used: 0,
31+
overall_accuracy: 0.0,
32+
overall_wpm: 0.0,
33+
overall_cpm: 0.0,
34+
total_keystrokes: 0,
35+
total_mistakes: 0,
36+
best_stage_wpm: 0.0,
37+
worst_stage_wpm: f64::MAX,
38+
best_stage_accuracy: 0.0,
39+
worst_stage_accuracy: f64::MAX,
40+
session_score: 0.0,
41+
}
42+
}
43+
44+
pub fn add_stage_result(&mut self, _stage_name: String, metrics: TypingMetrics, engine: &ScoringEngine) {
45+
self.total_challenges_completed += 1;
46+
self.total_keystrokes += engine.total_chars();
47+
self.total_mistakes += metrics.mistakes;
48+
self.session_score += metrics.challenge_score;
49+
50+
// Track best/worst performance
51+
if metrics.wpm > self.best_stage_wpm {
52+
self.best_stage_wpm = metrics.wpm;
53+
}
54+
if metrics.wpm < self.worst_stage_wpm {
55+
self.worst_stage_wpm = metrics.wpm;
56+
}
57+
if metrics.accuracy > self.best_stage_accuracy {
58+
self.best_stage_accuracy = metrics.accuracy;
59+
}
60+
if metrics.accuracy < self.worst_stage_accuracy {
61+
self.worst_stage_accuracy = metrics.accuracy;
62+
}
63+
}
64+
65+
pub fn add_skip(&mut self) {
66+
self.total_skips_used += 1;
67+
self.total_challenges_attempted += 1;
68+
}
69+
70+
pub fn finalize_session(&mut self) {
71+
self.total_session_time = self.session_start_time.elapsed();
72+
self.total_challenges_attempted = self.total_challenges_completed + self.total_skips_used;
73+
74+
// Calculate overall metrics - simplified since we don't track individual stage times
75+
if self.total_session_time.as_secs() > 0 && self.total_keystrokes > 0 {
76+
self.overall_cpm = (self.total_keystrokes as f64 / self.total_session_time.as_secs_f64()) * 60.0;
77+
self.overall_wpm = self.overall_cpm / 5.0;
78+
self.overall_accuracy = ((self.total_keystrokes.saturating_sub(self.total_mistakes)) as f64 / self.total_keystrokes as f64) * 100.0;
79+
}
80+
81+
// Handle edge cases for worst performance
82+
if self.worst_stage_wpm == f64::MAX {
83+
self.worst_stage_wpm = 0.0;
84+
}
85+
if self.worst_stage_accuracy == f64::MAX {
86+
self.worst_stage_accuracy = 0.0;
87+
}
88+
}
89+
90+
pub fn get_session_completion_status(&self) -> String {
91+
match (self.total_challenges_completed, self.total_skips_used) {
92+
(0, 0) => "No challenges attempted".to_string(),
93+
(completed, 0) if completed > 0 => format!("Perfect session! {} challenges completed", completed),
94+
(completed, skips) => format!("{} completed, {} skipped", completed, skips),
95+
}
96+
}
97+
}
98+
99+
impl Default for SessionSummary {
100+
fn default() -> Self {
101+
Self::new()
102+
}
103+
}
104+
105+
#[derive(Clone)]
106+
pub struct SessionTracker {
107+
summary: SessionSummary,
108+
}
109+
110+
impl SessionTracker {
111+
pub fn new() -> Self {
112+
Self {
113+
summary: SessionSummary::new(),
114+
}
115+
}
116+
117+
pub fn record_stage_completion(&mut self, stage_name: String, metrics: TypingMetrics, engine: &ScoringEngine) {
118+
self.summary.add_stage_result(stage_name, metrics, engine);
119+
}
120+
121+
pub fn record_skip(&mut self) {
122+
self.summary.add_skip();
123+
}
124+
125+
pub fn finalize_and_get_summary(mut self) -> SessionSummary {
126+
self.summary.finalize_session();
127+
self.summary
128+
}
129+
130+
pub fn get_current_summary(&self) -> &SessionSummary {
131+
&self.summary
132+
}
133+
}
134+
135+
impl Default for SessionTracker {
136+
fn default() -> Self {
137+
Self::new()
138+
}
139+
}

0 commit comments

Comments
 (0)