From 97839e250ce1d067f63c0ae03f6f0967fcc60bc3 Mon Sep 17 00:00:00 2001 From: markichnich Date: Tue, 22 Aug 2023 17:54:07 +0200 Subject: [PATCH] major control scheme rework --- README.md | 52 ++++++++++++++++++++++----- src/main.rs | 61 ++++++++++++++++++++++++-------- src/state.rs | 78 ++++++++++++++++++++++++++++++++++++++++- src/sudoku/generator.rs | 22 +++++++++--- src/ui.rs | 13 ++++--- 5 files changed, 192 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index cd1c8e1..30ffe8d 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,27 @@ A basic tui sudoku game for your shell. ### Controls -The current control scheme adheres to vim-like keybindings: +The current control scheme adheres to vim-like keybindings and is modal. - `h, j, k, l` to move `left, down, up, right` - `H, J, K, L` to move 3 spaces at once -- `x` to delete a number -- `1-9` to place a number -- `q` to quit -This shall be reworked. 😼 +- `1-9` to preselect a number + +- Modes: + - `a` to enter Markup mode + - `i` to enter Edit mode + - `g` to enter Go mode + - `1-9` to move to block + - you then return to the previous mode + - `A` and `I` to enter Edit/Markup mode "once" + - do a single edit/mark + - you then return to the previous mode + - `` to return to Edit mode + +- `` to place/unplace preselected number/mark +- `x` to delete a number/mark +- `q` to quit ### Preview @@ -58,9 +70,12 @@ This is what the sudoku will be displayed like. - [ ] Final UI - - [ ] Final controls - - [ ] Preselect numbers - - [ ] Cell markups (perhaps with unicode block thingies?) + - [x] Final controls + - [x] Preselect numbers + - [x] Edit Mode to (re)place numbers + - [x] Markup Mode to mark where numbers could go + - [x] Go Mode to move to blocks 1-9 + - [x] Toggle Number/Mark with Space - [ ] Colored UI - [ ] Hightlight selected numbers - [ ] Hightlight selected markups @@ -68,3 +83,24 @@ This is what the sudoku will be displayed like. - [ ] Live timer - [ ] Scoreboard access - [ ] Difficulty selection + + +The Final UI design should include a sidebar, +that will look something like the following: + + +``` +┌────────┬────────┬────────┐ ┌───────┐ +│ │ │ 8 9 │ │ Hard │ <- Difficulty +│ 4 9 7 │ │ 6 │ │ 36/81 │ <- Completion in number of cells +│ 2 │ 3 1 │ 7 │ ├───────┤ +├────────┼────────┼────────┤ │ 01:22 │ <- Elapsed Time +│ 6 │ 9 7 │ 3 │ ├───────┤ +│ 3 │ 5 2 │ │ │> Edit │ <- Active Mode +│ 7 2 │ 1 3 │ 5 4 │ │ Mark │ +├────────┼────────┼────────┤ │ Go │ +│ 2 1 │ 3 7 │ 9 5 │ ├───────┤ +│ 5 │ 9 │ 3 4 │ │ [9] │ <- Preselected Number +│ │ 4 │ 6 1 │ │ 4 / 9 │ <- Completion of preselected Number +└────────┴────────┴────────┘ └───────┘ +``` diff --git a/src/main.rs b/src/main.rs index da7c885..6b1760e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use std::{io, time::Duration}; fn main() -> io::Result<()> { let mut screen = Screen::init(io::stdout()); - let mut state = State::init(Difficulty::Medium); + let mut state = State::init(Difficulty::Mid); loop { if poll(Duration::from_millis(250))? { @@ -26,36 +26,69 @@ fn main() -> io::Result<()> { Char('j') => state.move_cursor(Dir::Down), Char('k') => state.move_cursor(Dir::Up), Char('l') => state.move_cursor(Dir::Right), + Char('H') => state.move_cursor(Dir::FarLeft), Char('J') => state.move_cursor(Dir::FarDown), Char('K') => state.move_cursor(Dir::FarUp), Char('L') => state.move_cursor(Dir::FarRight), + + Char('i') => state.enter_mode(Mode::Edit), + Char('I') => state.enter_mode_once(Mode::Edit), + + Char('a') => state.enter_mode(Mode::Markup), + Char('A') => state.enter_mode_once(Mode::Markup), + + Char('g') | Char('G') => state.enter_mode_once(Mode::Go), + + Char(' ') => match state.mode { + Mode::Markup => { + state.toggle_current_mark(); + state.enter_next_mode(); + } + Mode::Edit => { + state.toggle_current_cell(); + state.enter_next_mode(); + if is_solution(&state.board) { + screen.deinit()?; + println!("you win"); + break; + } + } + _ => {} + }, + Char('x') => { - if state.current_cell_modifiable() { - *state.current_cell() = 0; + if state.current_cell_is_modifiable() { + match state.mode { + Mode::Go => {} + Mode::Edit => state.delete_current_cell(), + Mode::Markup => state.delete_current_mark(), + } } } - Char(num) if ('1'..='9').contains(&num) => { - if state.current_cell_modifiable() { - *state.current_cell() = num as u8 - b'0'; + + Char(num) if ('1'..='9').contains(&num) => match state.mode { + Mode::Go => { + let idx = (num as u8 - b'1') as usize; + state.move_cursor_to(1 + idx / 3 * 3, 1 + idx % 3 * 3); + state.enter_next_mode(); } - if is_solution(&state.board) { - screen.deinit()?; - println!("you win"); - break; - } - } - Char('q') => { + _ => state.preselect_num(num as u8 - b'0'), + }, + + Char('q') | Char('Q') => { screen.deinit()?; break; } + + Esc => state.enter_mode(Mode::Edit), _ => {} } } } screen.update_dimensions()?; - screen.render(state.board); + screen.render(&state); screen.draw(state.cur_row, state.cur_col)?; } diff --git a/src/state.rs b/src/state.rs index 8695ce6..542db90 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,9 +12,21 @@ pub enum Dir { FarRight, } +#[derive(Default, PartialEq, Clone, Copy)] +pub enum Mode { + #[default] + Edit, + Markup, + Go, +} + pub struct State { pub board: Board, pub modifiable: [[bool; 9]; 9], + pub markups: [[[bool; 9]; 9]; 9], + pub mode: Mode, + pub next_mode: Mode, + pub cur_num: u8, pub cur_row: usize, pub cur_col: usize, } @@ -27,6 +39,10 @@ impl State { Self { board, modifiable, + markups: [[[false; 9]; 9]; 9], + mode: Mode::default(), + next_mode: Mode::default(), + cur_num: 1, cur_row: 4, cur_col: 4, } @@ -44,7 +60,7 @@ impl State { modifiable } - pub fn current_cell_modifiable(&self) -> bool { + pub fn current_cell_is_modifiable(&self) -> bool { self.modifiable[self.cur_row][self.cur_col] } @@ -69,4 +85,64 @@ impl State { _ => self.cur_row, }; } + + pub fn preselect_num(&mut self, num: u8) { + self.cur_num = num; + } + + pub fn toggle_current_cell(&mut self) { + if self.current_cell_is_modifiable() { + *self.current_cell() = if *self.current_cell() == self.cur_num { + 0 + } else { + self.cur_num + } + } + } + + pub fn delete_current_cell(&mut self) { + if self.current_cell_is_modifiable() { + *self.current_cell() = 0; + } + } + + pub fn enter_mode(&mut self, mode: Mode) { + match mode { + Mode::Go => { + self.enter_mode_once(mode); + return; + } + Mode::Edit => self.mode = Mode::Edit, + Mode::Markup => self.mode = Mode::Markup, + } + self.next_mode = self.mode; + } + + pub fn enter_mode_once(&mut self, mode: Mode) { + if self.mode != mode { + self.next_mode = self.mode; + self.mode = mode; + } + } + + pub fn enter_next_mode(&mut self) { + self.mode = self.next_mode; + } + + pub fn move_cursor_to(&mut self, row: usize, col: usize) { + assert!( + row < 9 && col < 9, + "[-] Error: State::go_to: Can't move to row/column out of bounds." + ); + self.cur_row = row; + self.cur_col = col; + } + + pub fn toggle_current_mark(&mut self) { + self.markups[self.cur_row][self.cur_col][self.cur_num as usize] ^= true; + } + + pub fn delete_current_mark(&mut self) { + self.markups[self.cur_row][self.cur_col][self.cur_num as usize] = false; + } } diff --git a/src/sudoku/generator.rs b/src/sudoku/generator.rs index afd08b5..1db669e 100644 --- a/src/sudoku/generator.rs +++ b/src/sudoku/generator.rs @@ -5,14 +5,16 @@ use crate::generator::Difficulty::*; use crate::rand::{seq::SliceRandom, thread_rng}; use crate::sudoku::{Board, New}; +use std::fmt; + /// categories of difficulty, indicating how many /// empty spaces will be on a sudoku board. #[allow(unused)] pub enum Difficulty { Easy, - Medium, + Mid, Hard, - Extreme, + Expert, Custom(usize), } @@ -22,14 +24,26 @@ impl Difficulty { pub fn removal_count(&self) -> usize { match self { Easy => 35, - Medium => 45, + Mid => 45, Hard => 52, - Extreme => 62, + Expert => 62, Custom(x) => *x, } } } +impl fmt::Display for Difficulty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + match self { + Easy => write!(f, "Easy"), + Mid => write!(f, " Mid"), + Hard => write!(f, "Hard"), + Expert => write!(f, "Exprt"), + Custom(x) => write!(f, "C({})", x), + } + } +} + /// generate a random, unsolved sudoku board with a given `Difficulty`. pub fn generate_sudoku(difficulty: Difficulty) -> Board { let mut board = Board::new(); diff --git a/src/ui.rs b/src/ui.rs index d6563eb..73293cd 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,3 +1,4 @@ +use crate::state::*; use std::io; use crossterm::{ @@ -70,15 +71,13 @@ where Ok(()) } - pub fn render(&mut self, board: [[u8; 9]; 9]) { + pub fn render(&mut self, state: &State) { let mut lines = Vec::new(); - lines.push("┌────────┬────────┬────────┐".to_string().chars().collect()); + lines.push("┌────────┬────────┬────────┐".to_string()); - for (row, row_slice) in board.iter().enumerate() { + for (row, row_slice) in state.board.iter().enumerate() { match row { - 3 | 6 => { - lines.push("├────────┼────────┼────────┤".to_string().chars().collect()); - } + 3 | 6 => lines.push("├────────┼────────┼────────┤".to_string()), _ => {} } let mut line = String::new(); @@ -97,7 +96,7 @@ where line.push_str(" │"); lines.push(line); } - lines.push("└────────┴────────┴────────┘".to_string().chars().collect()); + lines.push("└────────┴────────┴────────┘".to_string()); let pad_hori = self.width / 2 + lines[0].chars().count() / 2; let pad_vert = self.height - lines.len();