From 25e1448e41ad3a405bc88f5b0a977d10dd78f7af Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 28 Jul 2025 11:37:27 -0600 Subject: [PATCH] Encapsulate password locker --- thecockpit/src/app.rs | 379 -------------------------- thecockpit/src/app/mod.rs | 132 +++++++++ thecockpit/src/app/password_locker.rs | 279 +++++++++++++++++++ 3 files changed, 411 insertions(+), 379 deletions(-) delete mode 100644 thecockpit/src/app.rs create mode 100644 thecockpit/src/app/mod.rs create mode 100644 thecockpit/src/app/password_locker.rs diff --git a/thecockpit/src/app.rs b/thecockpit/src/app.rs deleted file mode 100644 index 1e959bc..0000000 --- a/thecockpit/src/app.rs +++ /dev/null @@ -1,379 +0,0 @@ -use std::{borrow::Cow, cmp::Ordering, ops::Range}; - -use rand::{rngs::OsRng, Rng, TryRngCore}; -#[cfg(target_arch = "wasm32")] -use ratzilla::ratatui; - -use ratatui::{ - layout::{Constraint, Direction, Flex, Layout}, - style::{Color, Style, Stylize}, - widgets::{Block, Borders, Paragraph, Tabs, Widget}, - Frame, -}; -use tachyonfx::{fx, Effect, Interpolation, Shader}; -use web_time::Instant; - -enum Tab { - Passcode, - Rest, -} - -pub struct App { - transition_instant: Instant, - selected_tab: usize, - current_number_guess: String, - binary_search_hint: BinarySearchHint, - tab: Tab, - current_effect: Effect, -} - -const SPLASH: &str = r#" -!~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -!~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -!!~~~~~~!J5PPPPPPPPPPP?^^^^7PPPPPPPPPPPPPPPPY~^^^^ -!!!!!!~~~!JB&@@@@@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^ -!!!!!5?!~~~!JG&@@@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^ -!!!!7#&GJ!~~~!?G&@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^ -7!!!7#@@&BJ!~~~!?G&@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^ -7!!!7#@@@@@BY!~~~~?P&@5~^^^Y@@@@@@@@@@@@@@@@#~^^^^ -777!7#@@@@@@@BY!~~~~7PY~~~^Y@@@@@@@@@@@@@@@@#~^^^^ -7777?#@@@@@@@@@#Y7~~~~!~~~~Y@@@@@@@@@@@@@@@@#~^^^^ -7777?#@@@@@@@@@@@#57~~~~~~~Y@@@@@@@@@@@@@@@@#~^^^^ -?777?#@@@@@@@@@@@@@#57~~~~~?#@@@@@@@@@@@@@@@#~^^^^ -?777?#@@@@@@@@@@@@@@@#J~~~~~!Y#@@@@@@@@@@@@@#~^^^^ -???7?#@@@@@@@@@@@@@@@@P!!~~~~~!YB@@@@@@@@@@@#~^^^^ -?????#@@@@@@@@@@@@@@@@P!!!!!!~~~!YB@@@@@@@@@#~^^^^ -????J&@@@@@@@@@@@@@@@@P!!!!YG?!~~~!JB&@@@@@@#~^^^^ -????J&@@@@@@@@@@@@@@@@P!!!!5@&G?!~~~!JG&@@@@#~^^^^ -J???J&@@@@@@@@@@@@@@@@P!!!!5@@@&GJ!~~~!?G&@@#!^^^^ -J???J&@@@@@@@@@@@@@@@@P7!!!5@@@@@&BJ!~~~!?G&#!^^^~ -JJJ?J&@@@@@@@@@@@@@@@@P777!5@@@@@@@@BY!~~~~?5!~~^~ -JJJJJ&@@@@@@@@@@@@@@@@P77775@@@@@@@@@@BY!~~~~~~~~~ -JJJJJGBBGGGGGGGGGGGGGGY7777JGGGGGGGPPPPPY!~~~~~~~~ -YJJJJJJJJJ????????????77777777777!!!!!!!!!!!~~~~~~ -YJJJJJJJJJJJ???????????77777777777!!!!!!!!!!!~~~~~ -"#; - -const TABS: [&'static str; 3] = ["Whatzahoozits", "Thingamajigs", "Doohickeys"]; -const NUMBERS: [u32; 3] = [0, 4, 9]; -const PASSWORD_BACKGROUND: Color = Color::Rgb(35, 35, 35); - -impl App { - pub fn new() -> Self { - Self { - transition_instant: Instant::now(), - selected_tab: 0, - current_number_guess: String::new(), - binary_search_hint: BinarySearchHint::default(), - tab: Tab::Passcode, - current_effect: fx::fade_from_fg(Color::Black, (200000, Interpolation::CircIn)), - } - } - - pub fn draw(&mut self, frame: &mut Frame) { - match self.tab { - Tab::Passcode => { - self.draw_passcode(frame); - } - Tab::Rest => { - self.draw_rest(frame); - } - } - let area = frame.area(); - if self.current_effect.done() - && self - .binary_search_hint - .guess_hint - .map(|(a, b)| a == b) - .unwrap_or_default() - { - self.tab = Tab::Rest; - self.transition_instant = Instant::now(); - self.current_effect = fx::slide_in( - tachyonfx::Motion::UpToDown, - 10, - 0, - PASSWORD_BACKGROUND, - (200000, Interpolation::Linear), - ); - self.binary_search_hint.guess_hint = None; - } - self.current_effect.process( - self.transition_instant.elapsed().into(), - frame.buffer_mut(), - area, - ); - } - - pub fn draw_passcode(&mut self, frame: &mut Frame) { - frame.render_widget(Block::new().bg(PASSWORD_BACKGROUND), frame.area()); - let middle_vertical = Layout::new(Direction::Vertical, [Constraint::Length(8)]) - .flex(Flex::Center) - .split(frame.area())[0]; - - let middle = Layout::new(Direction::Horizontal, [Constraint::Length(28)]) - .flex(Flex::Center) - .split(middle_vertical)[0]; - - let layout = Layout::new( - Direction::Vertical, - [ - Constraint::Length(1), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(1), - Constraint::Min(0), - ], - ) - .split(middle); - - let layout_code_blocks = Layout::new( - Direction::Horizontal, - [ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - ], - ) - .flex(Flex::SpaceBetween) - .split(layout[2]); - - frame.render_widget( - Paragraph::new("Enter the secret code") - .alignment(ratatui::layout::Alignment::Center) - .fg(Color::Rgb(226, 190, 89)) - .bg(PASSWORD_BACKGROUND), - layout[0], - ); - frame.render_widget(&self.binary_search_hint, layout[1]); - - let mut cursor_set = false; - for (block, num) in layout_code_blocks.iter().copied().zip( - self.binary_search_hint - .numbers_guessed - .iter() - .map(|num| Cow::Owned(format!("{}", *num))) - .chain(std::iter::repeat(Cow::Borrowed(" "))), - ) { - if !cursor_set && num == " " && self.current_effect.done() { - frame.set_cursor_position((block.x + 1, block.y + 1)); - cursor_set = true; - } - frame.render_widget( - Paragraph::new(num) - .block(Block::new().borders(Borders::all())) - .fg(Color::Rgb(226, 190, 89)) - .bg(PASSWORD_BACKGROUND), - block, - ); - } - if self.binary_search_hint.failed_guesses >= 20 { - frame.render_widget( - Paragraph::new("Search the binary") - .alignment(ratatui::layout::Alignment::Center) - .fg(Color::Rgb(226, 190, 89)) - .bg(PASSWORD_BACKGROUND), - layout[3], - ); - } else { - frame.render_widget( - Paragraph::new("Maximize efficiency") - .alignment(ratatui::layout::Alignment::Center) - .fg(Color::Rgb(226, 190, 89)) - .bg(PASSWORD_BACKGROUND), - layout[3], - ); - } - } - - pub fn draw_rest(&mut self, frame: &mut Frame) { - let layout = Layout::new( - Direction::Vertical, - [Constraint::Length(3), Constraint::Min(0)], - ) - .split(frame.area()); - frame.render_widget( - Tabs::new(TABS) - .select(self.selected_tab) - .fg(Color::Rgb(226, 190, 89)) - .bg(Color::Black) - .highlight_style(Style::new().bold()) - .block(Block::new().borders(Borders::all()).title("Coming soon")), - layout[0], - ); - frame.render_widget( - Paragraph::new(SPLASH) - .alignment(ratatui::layout::Alignment::Center) - .fg(Color::Rgb(226, 190, 89)) - .bg(Color::Black) - .block(Block::new().borders(Borders::all())), - layout[1], - ); - } - - pub fn add_char_to_number_guess(&mut self, c: char) { - self.current_number_guess.push(c); - if let Some(number) = c.to_digit(10) { - if self.binary_search_hint.guess_hint.is_some() { - self.binary_search_hint.numbers_guessed.clear(); - self.binary_search_hint.range = 0..10; - self.binary_search_hint.guess_hint = None; - } - self.binary_search_hint - .numbers_guessed - .push(number) - .unwrap(); - let guess_hint = self.binary_search_hint.range.start - + ((self.binary_search_hint.range.end - self.binary_search_hint.range.start) / 2); - if number != guess_hint { - self.binary_search_hint.guess_hint = Some((number, guess_hint)); - self.binary_search_hint.numbers_guessed.clear(); - self.binary_search_hint.number_to_guess += 1; - self.binary_search_hint.failed_guesses += 1; - if self.binary_search_hint.number_to_guess == NUMBERS.len() { - self.binary_search_hint.number_to_guess = 0; - } - #[cfg(target_arch = "wasm32")] - match self.binary_search_hint.number_to_guess { - 0 => ratzilla::web_sys::console::log_1(&"Secret code: 5210".into()), - 1 => ratzilla::web_sys::console::log_1(&"Secret code: 5234".into()), - 2 => ratzilla::web_sys::console::log_1(&"Secret code: 5789".into()), - _ => {} - } - return; - } - match number.cmp(&NUMBERS[self.binary_search_hint.number_to_guess]) { - Ordering::Less => { - self.binary_search_hint.range = number..self.binary_search_hint.range.end; - } - Ordering::Greater => { - self.binary_search_hint.range = self.binary_search_hint.range.start..number; - } - Ordering::Equal => { - self.binary_search_hint.guess_hint = Some((number, number)); - self.transition_instant = Instant::now(); - self.current_effect = fx::parallel(&[ - fx::effect_fn((), (200000, Interpolation::CircIn), |_, _, cells| { - cells.for_each(|(_, c)| c.fg = Color::Green); - }), - fx::fade_to_fg(PASSWORD_BACKGROUND, (200000, Interpolation::CircIn)), - ]); - } - } - } - } - - pub fn next_tab(&mut self) { - self.transition_instant = Instant::now(); - if self.selected_tab == TABS.len() - 1 { - self.selected_tab = 0; - } else { - self.selected_tab += 1; - } - } - - pub fn prev_tab(&mut self) { - self.transition_instant = Instant::now(); - if self.selected_tab == 0 { - self.selected_tab = TABS.len() - 1; - } else { - self.selected_tab -= 1; - } - } -} - -const MEASURING_TAPE: [&'static str; 4] = ["⣇", "⣄", "⣆", "⣄"]; - -struct BinarySearchHint { - numbers_guessed: heapless::Vec, - guess_hint: Option<(u32, u32)>, - range: Range, - number_to_guess: usize, - failed_guesses: usize, -} - -impl Default for BinarySearchHint { - fn default() -> Self { - let this = Self { - numbers_guessed: heapless::Vec::new(), - range: 0..10, - guess_hint: None, - number_to_guess: OsRng.unwrap_err().random_range(0..2), - failed_guesses: 0, - }; - #[cfg(target_arch = "wasm32")] - match this.number_to_guess { - 0 => ratzilla::web_sys::console::log_1(&"Secret code: 5210".into()), - 1 => ratzilla::web_sys::console::log_1(&"Secret code: 5234".into()), - 2 => ratzilla::web_sys::console::log_1(&"Secret code: 5789".into()), - _ => {} - } - this - } -} - -impl Widget for &BinarySearchHint { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) - where - Self: Sized, - { - let ruler_scale = 9.0 / area.width as f64; - for (number_on, (x, measures)) in (area.x..area.right()) - .zip(std::iter::from_fn(|| Some(MEASURING_TAPE.iter())).flatten()) - .enumerate() - { - let number_on = (number_on as f64 * ruler_scale) as u32; - let mut style = Style::default() - .fg(Color::Rgb(226, 190, 89)) - .bg(PASSWORD_BACKGROUND); - if !self.range.contains(&number_on) { - style = style.fg(Color::Rgb(117, 97, 42)); - } - buf.set_string(x, area.y + 1, measures, style); - } - - for number_guessed in &self.numbers_guessed { - let number_x = area.x + (*number_guessed as f64 / ruler_scale) as u16; - let style = Style::default() - .fg(Color::Rgb(226, 190, 89)) - .bg(PASSWORD_BACKGROUND); - buf.set_string(number_x, area.y + 1, "|", style); - if self.failed_guesses >= 30 { - buf.set_string(number_x, area.y, format!("{}", *number_guessed), style); - } - match NUMBERS[self.number_to_guess].cmp(number_guessed) { - Ordering::Less => buf.set_string(number_x, area.y + 2, "<", style), - Ordering::Greater => buf.set_string(number_x, area.y + 2, ">", style), - Ordering::Equal => buf.set_string(number_x, area.y + 2, "=", style), - } - } - - for number_hint in [0, 9] { - let number_x = area.x + (number_hint as f64 / ruler_scale) as u16; - let style = Style::default() - .fg(Color::Rgb(226, 190, 89)) - .bg(PASSWORD_BACKGROUND); - buf.set_string(number_x, area.y + 1, "|", style); - buf.set_string(number_x, area.y, format!("{}", number_hint), style); - } - - if self.failed_guesses >= 30 { - let guess_hint = self.range.start + ((self.range.end - self.range.start) / 2); - let number_x = area.x + (guess_hint as f64 / ruler_scale) as u16; - let style = Style::default() - .fg(Color::Rgb(226, 190, 89)) - .bg(PASSWORD_BACKGROUND); - buf.set_string(number_x, area.y + 2, "↑", style); - } - - if let Some((last_guessed, guess_hint)) = self.guess_hint { - let number_x = area.x + (last_guessed as f64 / ruler_scale) as u16; - let style = Style::default().fg(Color::Red).bg(PASSWORD_BACKGROUND); - buf.set_string(number_x, area.y + 1, "✗", style); - let number_x = area.x + (guess_hint as f64 / ruler_scale) as u16; - let style = Style::default().fg(Color::Green).bg(PASSWORD_BACKGROUND); - buf.set_string(number_x, area.y + 1, "✓", style); - } - } -} diff --git a/thecockpit/src/app/mod.rs b/thecockpit/src/app/mod.rs new file mode 100644 index 0000000..1310725 --- /dev/null +++ b/thecockpit/src/app/mod.rs @@ -0,0 +1,132 @@ +#[cfg(target_arch = "wasm32")] +use ratzilla::ratatui; + +use ratatui::{ + layout::{Constraint, Direction, Layout}, + style::{Color, Style, Stylize}, + widgets::{Block, Borders, Paragraph, Tabs}, + Frame, +}; +use tachyonfx::{fx, Effect, Interpolation, Shader}; +use web_time::Instant; + +use crate::app::password_locker::PasswordLocker; + +mod password_locker; + +pub struct App { + transition_instant: Instant, + selected_tab: usize, + password_locker: PasswordLocker, + current_effect: Effect, +} + +const SPLASH: &str = r#" +!~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +!~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +!!~~~~~~!J5PPPPPPPPPPP?^^^^7PPPPPPPPPPPPPPPPY~^^^^ +!!!!!!~~~!JB&@@@@@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^ +!!!!!5?!~~~!JG&@@@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^ +!!!!7#&GJ!~~~!?G&@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^ +7!!!7#@@&BJ!~~~!?G&@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^ +7!!!7#@@@@@BY!~~~~?P&@5~^^^Y@@@@@@@@@@@@@@@@#~^^^^ +777!7#@@@@@@@BY!~~~~7PY~~~^Y@@@@@@@@@@@@@@@@#~^^^^ +7777?#@@@@@@@@@#Y7~~~~!~~~~Y@@@@@@@@@@@@@@@@#~^^^^ +7777?#@@@@@@@@@@@#57~~~~~~~Y@@@@@@@@@@@@@@@@#~^^^^ +?777?#@@@@@@@@@@@@@#57~~~~~?#@@@@@@@@@@@@@@@#~^^^^ +?777?#@@@@@@@@@@@@@@@#J~~~~~!Y#@@@@@@@@@@@@@#~^^^^ +???7?#@@@@@@@@@@@@@@@@P!!~~~~~!YB@@@@@@@@@@@#~^^^^ +?????#@@@@@@@@@@@@@@@@P!!!!!!~~~!YB@@@@@@@@@#~^^^^ +????J&@@@@@@@@@@@@@@@@P!!!!YG?!~~~!JB&@@@@@@#~^^^^ +????J&@@@@@@@@@@@@@@@@P!!!!5@&G?!~~~!JG&@@@@#~^^^^ +J???J&@@@@@@@@@@@@@@@@P!!!!5@@@&GJ!~~~!?G&@@#!^^^^ +J???J&@@@@@@@@@@@@@@@@P7!!!5@@@@@&BJ!~~~!?G&#!^^^~ +JJJ?J&@@@@@@@@@@@@@@@@P777!5@@@@@@@@BY!~~~~?5!~~^~ +JJJJJ&@@@@@@@@@@@@@@@@P77775@@@@@@@@@@BY!~~~~~~~~~ +JJJJJGBBGGGGGGGGGGGGGGY7777JGGGGGGGPPPPPY!~~~~~~~~ +YJJJJJJJJJ????????????77777777777!!!!!!!!!!!~~~~~~ +YJJJJJJJJJJJ???????????77777777777!!!!!!!!!!!~~~~~ +"#; + +const TABS: [&'static str; 3] = ["Whatzahoozits", "Thingamajigs", "Doohickeys"]; + +impl App { + pub fn new() -> Self { + Self { + transition_instant: Instant::now(), + selected_tab: 0, + password_locker: PasswordLocker::default(), + current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)), + } + } + + pub fn draw(&mut self, frame: &mut Frame) { + if self.password_locker.unlocked() { + self.draw_rest(frame); + } + self.draw_passcode(frame); + let area = frame.area(); + + self.current_effect.process( + self.transition_instant.elapsed().into(), + frame.buffer_mut(), + area, + ); + } + + pub fn draw_passcode(&mut self, frame: &mut Frame) { + frame.render_widget(&mut self.password_locker, frame.area()); + if let Some(cursor_position) = self.password_locker.cursor_position() { + if self.password_locker.current_effect_done() { + frame.set_cursor_position(cursor_position); + } + } + } + + pub fn draw_rest(&mut self, frame: &mut Frame) { + let layout = Layout::new( + Direction::Vertical, + [Constraint::Length(3), Constraint::Min(0)], + ) + .split(frame.area()); + frame.render_widget( + Tabs::new(TABS) + .select(self.selected_tab) + .fg(Color::Rgb(226, 190, 89)) + .bg(Color::Black) + .highlight_style(Style::new().bold()) + .block(Block::new().borders(Borders::all()).title("Coming soon")), + layout[0], + ); + frame.render_widget( + Paragraph::new(SPLASH) + .alignment(ratatui::layout::Alignment::Center) + .fg(Color::Rgb(226, 190, 89)) + .bg(Color::Black) + .block(Block::new().borders(Borders::all())), + layout[1], + ); + } + + pub fn add_char_to_number_guess(&mut self, c: char) { + self.password_locker.add_char_to_number_guess(c); + } + + pub fn next_tab(&mut self) { + self.transition_instant = Instant::now(); + if self.selected_tab == TABS.len() - 1 { + self.selected_tab = 0; + } else { + self.selected_tab += 1; + } + } + + pub fn prev_tab(&mut self) { + self.transition_instant = Instant::now(); + if self.selected_tab == 0 { + self.selected_tab = TABS.len() - 1; + } else { + self.selected_tab -= 1; + } + } +} diff --git a/thecockpit/src/app/password_locker.rs b/thecockpit/src/app/password_locker.rs new file mode 100644 index 0000000..98b8dae --- /dev/null +++ b/thecockpit/src/app/password_locker.rs @@ -0,0 +1,279 @@ +use std::{borrow::Cow, cmp::Ordering, ops::Range}; + +use rand::{rngs::OsRng, Rng, TryRngCore}; +use ratatui::{ + buffer::Buffer, + layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect}, + style::{Color, Style, Stylize}, + widgets::{Block, Borders, Paragraph, Widget}, +}; +use tachyonfx::{fx, Effect, Interpolation, Shader}; +use web_time::Instant; + +const MEASURING_TAPE: [&'static str; 4] = ["⣇", "⣄", "⣆", "⣄"]; +const NUMBERS: [u32; 3] = [0, 4, 9]; +pub const PASSWORD_BACKGROUND: Color = Color::Rgb(35, 35, 35); + +pub struct PasswordLocker { + numbers_guessed: heapless::Vec, + transition_instant: Instant, + guess_hint: Option<(u32, u32)>, + range: Range, + number_to_guess: usize, + failed_guesses: usize, + cursor_position: Option, + current_effect: Effect, + unlocked: bool, +} + +impl Default for PasswordLocker { + fn default() -> Self { + let this = Self { + numbers_guessed: heapless::Vec::new(), + transition_instant: Instant::now(), + range: 0..10, + guess_hint: None, + number_to_guess: OsRng.unwrap_err().random_range(0..2), + failed_guesses: 0, + cursor_position: None, + current_effect: fx::fade_from_fg(Color::Black, (200000, Interpolation::CircIn)), + unlocked: false, + }; + #[cfg(target_arch = "wasm32")] + match this.number_to_guess { + 0 => ratzilla::web_sys::console::log_1(&"Secret code: 5210".into()), + 1 => ratzilla::web_sys::console::log_1(&"Secret code: 5234".into()), + 2 => ratzilla::web_sys::console::log_1(&"Secret code: 5789".into()), + _ => {} + } + this + } +} + +impl PasswordLocker { + pub fn add_char_to_number_guess(&mut self, c: char) -> bool { + if let Some(number) = c.to_digit(10) { + if self.guess_hint.is_some() { + self.numbers_guessed.clear(); + self.range = 0..10; + self.guess_hint = None; + } + self.numbers_guessed.push(number).unwrap(); + let guess_hint = self.range.start + ((self.range.end - self.range.start) / 2); + if number != guess_hint { + self.guess_hint = Some((number, guess_hint)); + self.numbers_guessed.clear(); + self.number_to_guess += 1; + self.failed_guesses += 1; + if self.number_to_guess == NUMBERS.len() { + self.number_to_guess = 0; + } + #[cfg(target_arch = "wasm32")] + match self.number_to_guess { + 0 => ratzilla::web_sys::console::log_1(&"Secret code: 5210".into()), + 1 => ratzilla::web_sys::console::log_1(&"Secret code: 5234".into()), + 2 => ratzilla::web_sys::console::log_1(&"Secret code: 5789".into()), + _ => {} + } + return false; + } + match number.cmp(&NUMBERS[self.number_to_guess]) { + Ordering::Less => { + self.range = number..self.range.end; + } + Ordering::Greater => { + self.range = self.range.start..number; + } + Ordering::Equal => { + self.guess_hint = Some((number, number)); + self.transition_instant = Instant::now(); + self.current_effect = fx::parallel(&[ + fx::effect_fn((), (200000, Interpolation::CircIn), |_, _, cells| { + cells.for_each(|(_, c)| c.fg = Color::Green); + }), + fx::fade_to_fg(PASSWORD_BACKGROUND, (200000, Interpolation::CircIn)), + ]); + return true; + } + } + } + + false + } + + pub fn done(&self) -> bool { + self.guess_hint.map(|(a, b)| a == b).unwrap_or_default() + } + + pub fn current_effect_done(&self) -> bool { + self.current_effect.done() + } + + pub fn reset(&mut self) { + self.numbers_guessed.clear(); + self.guess_hint = None; + self.range = 0..10; + } + + pub fn cursor_position(&self) -> Option { + self.cursor_position + } + + pub fn unlocked(&self) -> bool { + self.unlocked + } +} + +impl Widget for &mut PasswordLocker { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + if !self.unlocked { + Block::new().bg(PASSWORD_BACKGROUND).render(area, buf); + + let middle_vertical = Layout::new(Direction::Vertical, [Constraint::Length(8)]) + .flex(Flex::Center) + .split(area)[0]; + + let middle = Layout::new(Direction::Horizontal, [Constraint::Length(28)]) + .flex(Flex::Center) + .split(middle_vertical)[0]; + + let layout = Layout::new( + Direction::Vertical, + [ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Min(0), + ], + ) + .split(middle); + + let layout_code_blocks = Layout::new( + Direction::Horizontal, + [ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ], + ) + .flex(Flex::SpaceBetween) + .split(layout[2]); + + Paragraph::new("Enter the secret code") + .alignment(Alignment::Center) + .fg(Color::Rgb(226, 190, 89)) + .bg(PASSWORD_BACKGROUND) + .render(layout[0], buf); + + let mut cursor_set = false; + self.cursor_position = None; + for (block, num) in layout_code_blocks.iter().copied().zip( + self.numbers_guessed + .iter() + .map(|num| Cow::Owned(format!("{}", *num))) + .chain(std::iter::repeat(Cow::Borrowed(" "))), + ) { + if !cursor_set && num == " " { + self.cursor_position = Some(Position { + x: block.x + 1, + y: block.y + 1, + }); + cursor_set = true; + } + Paragraph::new(num) + .block(Block::new().borders(Borders::all())) + .fg(Color::Rgb(226, 190, 89)) + .bg(PASSWORD_BACKGROUND) + .render(block, buf); + } + + Paragraph::new(if self.failed_guesses >= 20 { + "Search the binary" + } else { + "Maximize efficiency" + }) + .alignment(Alignment::Center) + .fg(Color::Rgb(226, 190, 89)) + .bg(PASSWORD_BACKGROUND) + .render(layout[3], buf); + + let ruler_scale = 9.0 / middle.width as f64; + for (number_on, (x, measures)) in (middle.x..middle.right()) + .zip(std::iter::from_fn(|| Some(MEASURING_TAPE.iter())).flatten()) + .enumerate() + { + let number_on = (number_on as f64 * ruler_scale) as u32; + let mut style = Style::default() + .fg(Color::Rgb(226, 190, 89)) + .bg(PASSWORD_BACKGROUND); + if !self.range.contains(&number_on) { + style = style.fg(Color::Rgb(117, 97, 42)); + } + buf.set_string(x, middle.y + 1, measures, style); + } + + for number_guessed in &self.numbers_guessed { + let number_x = middle.x + (*number_guessed as f64 / ruler_scale) as u16; + let style = Style::default() + .fg(Color::Rgb(226, 190, 89)) + .bg(PASSWORD_BACKGROUND); + buf.set_string(number_x, middle.y + 1, "|", style); + if self.failed_guesses >= 30 { + buf.set_string(number_x, middle.y, format!("{}", *number_guessed), style); + } + match NUMBERS[self.number_to_guess].cmp(number_guessed) { + Ordering::Less => buf.set_string(number_x, middle.y + 2, "<", style), + Ordering::Greater => buf.set_string(number_x, middle.y + 2, ">", style), + Ordering::Equal => buf.set_string(number_x, middle.y + 2, "=", style), + } + } + + for number_hint in [0, 9] { + let number_x = middle.x + (number_hint as f64 / ruler_scale) as u16; + let style = Style::default() + .fg(Color::Rgb(226, 190, 89)) + .bg(PASSWORD_BACKGROUND); + buf.set_string(number_x, middle.y + 1, "|", style); + buf.set_string(number_x, middle.y, format!("{}", number_hint), style); + } + + if self.failed_guesses >= 30 { + let guess_hint = self.range.start + ((self.range.end - self.range.start) / 2); + let number_x = middle.x + (guess_hint as f64 / ruler_scale) as u16; + let style = Style::default() + .fg(Color::Rgb(226, 190, 89)) + .bg(PASSWORD_BACKGROUND); + buf.set_string(number_x, middle.y + 2, "↑", style); + } + + if let Some((last_guessed, guess_hint)) = self.guess_hint { + let number_x = middle.x + (last_guessed as f64 / ruler_scale) as u16; + let style = Style::default().fg(Color::Red).bg(PASSWORD_BACKGROUND); + buf.set_string(number_x, middle.y + 1, "✗", style); + let number_x = middle.x + (guess_hint as f64 / ruler_scale) as u16; + let style = Style::default().fg(Color::Green).bg(PASSWORD_BACKGROUND); + buf.set_string(number_x, middle.y + 1, "✓", style); + } + + if self.current_effect.done() && self.done() { + self.transition_instant = Instant::now(); + self.current_effect = fx::slide_in( + tachyonfx::Motion::UpToDown, + 10, + 0, + PASSWORD_BACKGROUND, + (200000, Interpolation::Linear), + ); + self.unlocked = true; + self.reset(); + } + } + self.current_effect + .process(self.transition_instant.elapsed().into(), buf, area); + } +}