Skip to content

Commit 33accf7

Browse files
unhappychoiceclaude
andcommitted
feat: implement retry option on failure and cancellation screens
close: #128 - Add FailureScreen with retry functionality for failed challenges - Add CancelScreen with retry functionality for cancelled challenges - Both screens provide [R] Retry, [T] Back to Title, [ESC] Session Summary options - Users can now retry challenges without restarting the application 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 05657c3 commit 33accf7

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed

src/game/screens/cancel_screen.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use crate::game::screens::session_summary_screen::ResultAction;
2+
use crate::scoring::{ScoringEngine, TypingMetrics};
3+
use crate::{extractor::GitRepositoryInfo, Result};
4+
use crossterm::{
5+
cursor::MoveTo,
6+
event::{self, Event, KeyCode, KeyModifiers},
7+
execute,
8+
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
9+
terminal::{self, ClearType},
10+
};
11+
use std::io::{stdout, Write};
12+
13+
pub struct CancelScreen;
14+
15+
impl CancelScreen {
16+
pub fn show_session_summary_cancel_mode(
17+
total_stages: usize,
18+
completed_stages: usize,
19+
stage_engines: &[(String, ScoringEngine)],
20+
_repo_info: &Option<GitRepositoryInfo>,
21+
) -> Result<ResultAction> {
22+
let mut stdout = stdout();
23+
24+
// Comprehensive screen reset
25+
execute!(stdout, terminal::Clear(ClearType::All))?;
26+
execute!(stdout, MoveTo(0, 0))?;
27+
execute!(stdout, ResetColor)?;
28+
stdout.flush()?;
29+
30+
// Short delay to ensure terminal state is reset
31+
std::thread::sleep(std::time::Duration::from_millis(10));
32+
33+
let (terminal_width, terminal_height) = terminal::size()?;
34+
let center_y = terminal_height / 2;
35+
36+
// Header - show CANCELLED status (centered)
37+
let header_text = "=== SESSION CANCELLED ===";
38+
let header_x = (terminal_width - header_text.len() as u16) / 2;
39+
execute!(stdout, MoveTo(header_x, center_y.saturating_sub(6)))?;
40+
execute!(
41+
stdout,
42+
SetForegroundColor(Color::Yellow),
43+
SetAttribute(Attribute::Bold)
44+
)?;
45+
execute!(stdout, Print(header_text))?;
46+
execute!(stdout, ResetColor)?;
47+
48+
// Show stage progress (centered, cyan)
49+
let stage_text = format!("Stages: {}/{}", completed_stages, total_stages);
50+
let stage_x = (terminal_width - stage_text.len() as u16) / 2;
51+
execute!(stdout, MoveTo(stage_x, center_y.saturating_sub(2)))?;
52+
execute!(stdout, SetForegroundColor(Color::Cyan))?;
53+
execute!(stdout, Print(stage_text))?;
54+
55+
// Show basic metrics if available (centered, white)
56+
if !stage_engines.is_empty() {
57+
let last_engine = &stage_engines.last().unwrap().1;
58+
let metrics = last_engine
59+
.calculate_metrics_with_status(false, false)
60+
.unwrap_or_else(|_| {
61+
// Create basic metrics if calculation fails
62+
TypingMetrics {
63+
cpm: last_engine.cpm(),
64+
wpm: last_engine.wpm(),
65+
accuracy: last_engine.accuracy(),
66+
mistakes: last_engine.mistakes(),
67+
consistency_streaks: vec![],
68+
completion_time: std::time::Duration::new(0, 0),
69+
challenge_score: 0.0,
70+
ranking_title: "Unranked".to_string(),
71+
ranking_tier: "Beginner".to_string(),
72+
tier_position: 0,
73+
tier_total: 0,
74+
overall_position: 0,
75+
overall_total: 0,
76+
was_skipped: false,
77+
was_failed: false,
78+
}
79+
});
80+
81+
let metrics_text = format!(
82+
"CPM: {:.0} | WPM: {:.0} | Accuracy: {:.0}%",
83+
metrics.cpm, metrics.wpm, metrics.accuracy
84+
);
85+
let metrics_x = (terminal_width - metrics_text.len() as u16) / 2;
86+
execute!(stdout, MoveTo(metrics_x, center_y))?;
87+
execute!(stdout, SetForegroundColor(Color::White))?;
88+
execute!(stdout, Print(metrics_text))?;
89+
}
90+
91+
// Cancellation message (centered)
92+
let cancel_text = "Challenge cancelled. You can retry or go back to title.";
93+
let cancel_x = (terminal_width - cancel_text.len() as u16) / 2;
94+
execute!(stdout, MoveTo(cancel_x, center_y + 2))?;
95+
execute!(stdout, SetForegroundColor(Color::Yellow))?;
96+
execute!(stdout, Print(cancel_text))?;
97+
98+
// Navigation instructions with color coding
99+
let full_text_len = "[R] Retry | [T] Back to Title | [ESC] Session Summary & Exit".len();
100+
let nav_x = (terminal_width - full_text_len as u16) / 2;
101+
execute!(stdout, MoveTo(nav_x, center_y + 4))?;
102+
execute!(stdout, SetForegroundColor(Color::Green))?;
103+
execute!(stdout, Print("[R]"))?;
104+
execute!(stdout, SetForegroundColor(Color::White))?;
105+
execute!(stdout, Print(" Retry | "))?;
106+
execute!(stdout, SetForegroundColor(Color::Green))?;
107+
execute!(stdout, Print("[T]"))?;
108+
execute!(stdout, SetForegroundColor(Color::White))?;
109+
execute!(stdout, Print(" Back to Title | "))?;
110+
execute!(stdout, SetForegroundColor(Color::Red))?;
111+
execute!(stdout, Print("[ESC]"))?;
112+
execute!(stdout, SetForegroundColor(Color::White))?;
113+
execute!(stdout, Print(" Session Summary & Exit"))?;
114+
115+
execute!(stdout, ResetColor)?;
116+
stdout.flush()?;
117+
118+
// Wait for user input and return action
119+
loop {
120+
if event::poll(std::time::Duration::from_millis(100))? {
121+
if let Event::Key(key_event) = event::read()? {
122+
match key_event.code {
123+
KeyCode::Char('r') | KeyCode::Char('R') => {
124+
return Ok(ResultAction::Retry);
125+
}
126+
KeyCode::Char('t') | KeyCode::Char('T') => {
127+
return Ok(ResultAction::BackToTitle);
128+
}
129+
KeyCode::Esc => {
130+
return Ok(ResultAction::Quit);
131+
}
132+
KeyCode::Char('c')
133+
if key_event.modifiers.contains(KeyModifiers::CONTROL) =>
134+
{
135+
return Ok(ResultAction::Quit);
136+
}
137+
_ => continue,
138+
}
139+
}
140+
}
141+
}
142+
}
143+
}

src/game/screens/failure_screen.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use crate::game::screens::session_summary_screen::ResultAction;
2+
use crate::scoring::ScoringEngine;
3+
use crate::{extractor::GitRepositoryInfo, Result};
4+
use crossterm::{
5+
cursor::MoveTo,
6+
event::{self, Event, KeyCode, KeyModifiers},
7+
execute,
8+
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
9+
terminal::{self, ClearType},
10+
};
11+
use std::io::{stdout, Write};
12+
13+
pub struct FailureScreen;
14+
15+
impl FailureScreen {
16+
pub fn show_session_summary_fail_mode(
17+
total_stages: usize,
18+
completed_stages: usize,
19+
stage_engines: &[(String, ScoringEngine)],
20+
_repo_info: &Option<GitRepositoryInfo>,
21+
) -> Result<ResultAction> {
22+
let mut stdout = stdout();
23+
24+
// Comprehensive screen reset
25+
execute!(stdout, terminal::Clear(ClearType::All))?;
26+
execute!(stdout, MoveTo(0, 0))?;
27+
execute!(stdout, ResetColor)?;
28+
stdout.flush()?;
29+
30+
// Short delay to ensure terminal state is reset
31+
std::thread::sleep(std::time::Duration::from_millis(10));
32+
33+
let (terminal_width, terminal_height) = terminal::size()?;
34+
let center_y = terminal_height / 2;
35+
36+
// Header - show FAILED status (centered)
37+
let header_text = "=== SESSION FAILED ===";
38+
let header_x = (terminal_width - header_text.len() as u16) / 2;
39+
execute!(stdout, MoveTo(header_x, center_y.saturating_sub(6)))?;
40+
execute!(
41+
stdout,
42+
SetForegroundColor(Color::Red),
43+
SetAttribute(Attribute::Bold)
44+
)?;
45+
execute!(stdout, Print(header_text))?;
46+
execute!(stdout, ResetColor)?;
47+
48+
// Show stage progress (centered, cyan)
49+
let stage_text = format!("Stages: {}/{}", completed_stages, total_stages);
50+
let stage_x = (terminal_width - stage_text.len() as u16) / 2;
51+
execute!(stdout, MoveTo(stage_x, center_y.saturating_sub(2)))?;
52+
execute!(stdout, SetForegroundColor(Color::Cyan))?;
53+
execute!(stdout, Print(stage_text))?;
54+
55+
// Show basic metrics if available (centered, white)
56+
if !stage_engines.is_empty() {
57+
let last_engine = &stage_engines.last().unwrap().1;
58+
let metrics = last_engine
59+
.calculate_metrics_with_status(false, true)
60+
.unwrap();
61+
62+
let metrics_text = format!(
63+
"CPM: {:.0} | WPM: {:.0} | Accuracy: {:.0}%",
64+
metrics.cpm, metrics.wpm, metrics.accuracy
65+
);
66+
let metrics_x = (terminal_width - metrics_text.len() as u16) / 2;
67+
execute!(stdout, MoveTo(metrics_x, center_y))?;
68+
execute!(stdout, SetForegroundColor(Color::White))?;
69+
execute!(stdout, Print(metrics_text))?;
70+
}
71+
72+
// Failure message (centered)
73+
let fail_text = "Challenge failed. Better luck next time!";
74+
let fail_x = (terminal_width - fail_text.len() as u16) / 2;
75+
execute!(stdout, MoveTo(fail_x, center_y + 2))?;
76+
execute!(stdout, SetForegroundColor(Color::Red))?;
77+
execute!(stdout, Print(fail_text))?;
78+
79+
// Navigation instructions with color coding
80+
let full_text_len = "[R] Retry | [T] Back to Title | [ESC] Session Summary & Exit".len();
81+
let nav_x = (terminal_width - full_text_len as u16) / 2;
82+
execute!(stdout, MoveTo(nav_x, center_y + 4))?;
83+
execute!(stdout, SetForegroundColor(Color::Green))?;
84+
execute!(stdout, Print("[R]"))?;
85+
execute!(stdout, SetForegroundColor(Color::White))?;
86+
execute!(stdout, Print(" Retry | "))?;
87+
execute!(stdout, SetForegroundColor(Color::Green))?;
88+
execute!(stdout, Print("[T]"))?;
89+
execute!(stdout, SetForegroundColor(Color::White))?;
90+
execute!(stdout, Print(" Back to Title | "))?;
91+
execute!(stdout, SetForegroundColor(Color::Red))?;
92+
execute!(stdout, Print("[ESC]"))?;
93+
execute!(stdout, SetForegroundColor(Color::White))?;
94+
execute!(stdout, Print(" Session Summary & Exit"))?;
95+
96+
execute!(stdout, ResetColor)?;
97+
stdout.flush()?;
98+
99+
// Wait for user input and return action
100+
loop {
101+
if event::poll(std::time::Duration::from_millis(100))? {
102+
if let Event::Key(key_event) = event::read()? {
103+
match key_event.code {
104+
KeyCode::Char('r') | KeyCode::Char('R') => {
105+
return Ok(ResultAction::Retry);
106+
}
107+
KeyCode::Char('t') | KeyCode::Char('T') => {
108+
return Ok(ResultAction::BackToTitle);
109+
}
110+
KeyCode::Esc => {
111+
return Ok(ResultAction::Quit);
112+
}
113+
KeyCode::Char('c')
114+
if key_event.modifiers.contains(KeyModifiers::CONTROL) =>
115+
{
116+
return Ok(ResultAction::Quit);
117+
}
118+
_ => continue,
119+
}
120+
}
121+
}
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)