diff --git a/thecockpit/.cargo/config.toml b/thecockpit/.cargo/config.toml new file mode 100644 index 0000000..2e07606 --- /dev/null +++ b/thecockpit/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] diff --git a/thecockpit/Cargo.toml b/thecockpit/Cargo.toml index 466d5c9..0d7401d 100644 --- a/thecockpit/Cargo.toml +++ b/thecockpit/Cargo.toml @@ -37,5 +37,9 @@ wasm-bindgen-test = "0.3.34" opt-level = "s" [dependencies] +getrandom = { version = "0.3.3", features = ["wasm_js"] } +heapless = "0.8.0" +rand = { version = "0.9.2", default-features = false, features = ["os_rng", "std"] } +tachyonfx = { version = "0.16.0", default-features = false, features = ["web-time"] } web-time = "1.1.0" diff --git a/thecockpit/ratzilla b/thecockpit/ratzilla index 96a321f..f1a086a 160000 --- a/thecockpit/ratzilla +++ b/thecockpit/ratzilla @@ -1 +1 @@ -Subproject commit 96a321f519c9c637b8568e40193c1f89c3efe4a0 +Subproject commit f1a086aa3ffa0c6e2a04f9306b77e560c8e224c9 diff --git a/thecockpit/src/app.rs b/thecockpit/src/app.rs index 0ac584f..c7a8805 100644 --- a/thecockpit/src/app.rs +++ b/thecockpit/src/app.rs @@ -1,17 +1,32 @@ +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, Layout}, - style::{Color, Stylize}, - widgets::{Block, Borders, Paragraph, Tabs}, + 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, + number_guess_responses: String, + binary_search_hint: BinarySearchHint, + tab: Tab, + current_effect: Effect, + failed_guesses: usize, } const SPLASH: &str = r#" @@ -42,16 +57,146 @@ YJJJJJJJJJJJ???????????77777777777!!!!!!!!!!!~~~~~ "#; const TABS: [&'static str; 3] = ["Whatzahoozits", "Thingamajigs", "Doohickeys"]; +const NUMBERS: [u32; 3] = [0, 4, 9]; impl App { pub fn new() -> Self { Self { transition_instant: Instant::now(), selected_tab: 0, + current_number_guess: String::new(), + binary_search_hint: BinarySearchHint::default(), + number_guess_responses: String::new(), + tab: Tab::Passcode, + current_effect: fx::fade_from_fg(Color::Black, (200000, Interpolation::CircIn)), + failed_guesses: 0, } } 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::fade_from_fg( + Color::Black, + ( + self.transition_instant.elapsed().as_secs() as u32 + 200000, + Interpolation::CircIn, + ), + ); + 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) { + let middle_vertical = Layout::new( + Direction::Vertical, + [ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ], + ) + .split(frame.area())[1]; + + let middle = Layout::new( + Direction::Horizontal, + [ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ], + ) + .split(middle_vertical)[1]; + + let layout = Layout::new( + Direction::Vertical, + [ + Constraint::Length(2), + Constraint::Length(2), + 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(Color::Black), + layout[0], + ); + frame.render_widget(&self.binary_search_hint, layout[1]); + + 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(" "))), + ) { + frame.render_widget( + Paragraph::new(num) + .block(Block::new().borders(Borders::all())) + .fg(Color::Rgb(226, 190, 89)) + .bg(Color::Black), + block, + ); + } + if self.failed_guesses >= 10 { + frame.render_widget( + Paragraph::new("Search the binary") + .alignment(ratatui::layout::Alignment::Center) + .fg(Color::Rgb(226, 190, 89)) + .bg(Color::Black), + layout[3], + ); + } else { + frame.render_widget( + Paragraph::new("Maximize efficiency") + .alignment(ratatui::layout::Alignment::Center) + .fg(Color::Rgb(226, 190, 89)) + .bg(Color::Black), + layout[3], + ); + } + } + + pub fn draw_rest(&mut self, frame: &mut Frame) { let layout = Layout::new( Direction::Vertical, [Constraint::Length(3), Constraint::Min(0)], @@ -75,6 +220,51 @@ impl App { ); } + 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.failed_guesses += 1; + if self.binary_search_hint.number_to_guess == NUMBERS.len() { + self.binary_search_hint.number_to_guess = 0; + } + 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(Color::Black, (200000, Interpolation::CircIn)), + ]); + } + } + } + } + pub fn next_tab(&mut self) { self.transition_instant = Instant::now(); if self.selected_tab == TABS.len() - 1 { @@ -93,3 +283,67 @@ impl App { } } } + +const MEASURING_TAPE: [&'static str; 4] = ["⣇", "⣄", "⣆", "⣄"]; + +struct BinarySearchHint { + numbers_guessed: heapless::Vec, + guess_hint: Option<(u32, u32)>, + range: Range, + number_to_guess: usize, +} + +impl Default for BinarySearchHint { + fn default() -> Self { + Self { + numbers_guessed: heapless::Vec::new(), + range: 0..10, + guess_hint: None, + number_to_guess: OsRng.unwrap_err().random_range(0..2), + } + } +} + +impl Widget for &BinarySearchHint { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let ruler_scale = 10.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(Color::Black); + if !self.range.contains(&number_on) { + style = style.fg(Color::Rgb(117, 97, 42)); + } + buf.set_string(x, area.y, 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(Color::Black); + buf.set_string(number_x, area.y, "|", style); + match NUMBERS[self.number_to_guess].cmp(number_guessed) { + Ordering::Less => buf.set_string(number_x, area.y + 1, "<", style), + Ordering::Greater => buf.set_string(number_x, area.y + 1, ">", style), + Ordering::Equal => buf.set_string(number_x, area.y + 1, "=", 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(Color::Black); + buf.set_string(number_x, area.y, "✗", style); + let number_x = area.x + (guess_hint as f64 / ruler_scale) as u16; + let style = Style::default().fg(Color::Green).bg(Color::Black); + buf.set_string(number_x, area.y, "✓", style); + } + } +} diff --git a/thecockpit/src/main.rs b/thecockpit/src/main.rs index b67ea5f..decc82e 100644 --- a/thecockpit/src/main.rs +++ b/thecockpit/src/main.rs @@ -8,7 +8,7 @@ fn main() { let mut app = App::new(); loop { - terminal.draw(|frame| app.draw(frame)); + terminal.draw(|frame| app.draw(frame)).unwrap(); if event::poll(Duration::from_secs(0)).unwrap() { match event::read().unwrap() { @@ -24,6 +24,10 @@ fn main() { code: event::KeyCode::Right, .. }) => app.next_tab(), + event::Event::Key(event::KeyEvent { + code: event::KeyCode::Char(c), + .. + }) => app.add_char_to_number_guess(c), _ => {} } } diff --git a/thecockpit/src/web/mod.rs b/thecockpit/src/web/mod.rs index fc0c526..4eb575f 100644 --- a/thecockpit/src/web/mod.rs +++ b/thecockpit/src/web/mod.rs @@ -39,6 +39,7 @@ pub fn run(grid_id: &str) { move |event| match event.code { KeyCode::Left => app.borrow_mut().prev_tab(), KeyCode::Right => app.borrow_mut().next_tab(), + KeyCode::Char(c) => app.borrow_mut().add_char_to_number_guess(c), _ => {} } });