major control scheme rework

This commit is contained in:
markichnich 2023-08-22 17:54:07 +02:00
parent 23e83cc9a5
commit 97839e250c
5 changed files with 192 additions and 34 deletions

View File

@ -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
- `<esc>` to return to Edit mode
- `<space>` 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
└────────┴────────┴────────┘ └───────┘
```

View File

@ -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)?;
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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();