major control scheme rework
This commit is contained in:
parent
23e83cc9a5
commit
97839e250c
52
README.md
52
README.md
@ -7,15 +7,27 @@ A basic tui sudoku game for your shell.
|
|||||||
|
|
||||||
### Controls
|
### 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 `left, down, up, right`
|
||||||
- `H, J, K, L` to move 3 spaces at once
|
- `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
|
### Preview
|
||||||
@ -58,9 +70,12 @@ This is what the sudoku will be displayed like.
|
|||||||
|
|
||||||
|
|
||||||
- [ ] Final UI
|
- [ ] Final UI
|
||||||
- [ ] Final controls
|
- [x] Final controls
|
||||||
- [ ] Preselect numbers
|
- [x] Preselect numbers
|
||||||
- [ ] Cell markups (perhaps with unicode block thingies?)
|
- [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
|
- [ ] Colored UI
|
||||||
- [ ] Hightlight selected numbers
|
- [ ] Hightlight selected numbers
|
||||||
- [ ] Hightlight selected markups
|
- [ ] Hightlight selected markups
|
||||||
@ -68,3 +83,24 @@ This is what the sudoku will be displayed like.
|
|||||||
- [ ] Live timer
|
- [ ] Live timer
|
||||||
- [ ] Scoreboard access
|
- [ ] Scoreboard access
|
||||||
- [ ] Difficulty selection
|
- [ ] 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
|
||||||
|
└────────┴────────┴────────┘ └───────┘
|
||||||
|
```
|
||||||
|
|||||||
61
src/main.rs
61
src/main.rs
@ -16,7 +16,7 @@ use std::{io, time::Duration};
|
|||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> io::Result<()> {
|
||||||
let mut screen = Screen::init(io::stdout());
|
let mut screen = Screen::init(io::stdout());
|
||||||
let mut state = State::init(Difficulty::Medium);
|
let mut state = State::init(Difficulty::Mid);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if poll(Duration::from_millis(250))? {
|
if poll(Duration::from_millis(250))? {
|
||||||
@ -26,36 +26,69 @@ fn main() -> io::Result<()> {
|
|||||||
Char('j') => state.move_cursor(Dir::Down),
|
Char('j') => state.move_cursor(Dir::Down),
|
||||||
Char('k') => state.move_cursor(Dir::Up),
|
Char('k') => state.move_cursor(Dir::Up),
|
||||||
Char('l') => state.move_cursor(Dir::Right),
|
Char('l') => state.move_cursor(Dir::Right),
|
||||||
|
|
||||||
Char('H') => state.move_cursor(Dir::FarLeft),
|
Char('H') => state.move_cursor(Dir::FarLeft),
|
||||||
Char('J') => state.move_cursor(Dir::FarDown),
|
Char('J') => state.move_cursor(Dir::FarDown),
|
||||||
Char('K') => state.move_cursor(Dir::FarUp),
|
Char('K') => state.move_cursor(Dir::FarUp),
|
||||||
Char('L') => state.move_cursor(Dir::FarRight),
|
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') => {
|
Char('x') => {
|
||||||
if state.current_cell_modifiable() {
|
if state.current_cell_is_modifiable() {
|
||||||
*state.current_cell() = 0;
|
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() {
|
Char(num) if ('1'..='9').contains(&num) => match state.mode {
|
||||||
*state.current_cell() = num as u8 - b'0';
|
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) {
|
_ => state.preselect_num(num as u8 - b'0'),
|
||||||
screen.deinit()?;
|
},
|
||||||
println!("you win");
|
|
||||||
break;
|
Char('q') | Char('Q') => {
|
||||||
}
|
|
||||||
}
|
|
||||||
Char('q') => {
|
|
||||||
screen.deinit()?;
|
screen.deinit()?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Esc => state.enter_mode(Mode::Edit),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
screen.update_dimensions()?;
|
screen.update_dimensions()?;
|
||||||
screen.render(state.board);
|
screen.render(&state);
|
||||||
screen.draw(state.cur_row, state.cur_col)?;
|
screen.draw(state.cur_row, state.cur_col)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
78
src/state.rs
78
src/state.rs
@ -12,9 +12,21 @@ pub enum Dir {
|
|||||||
FarRight,
|
FarRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum Mode {
|
||||||
|
#[default]
|
||||||
|
Edit,
|
||||||
|
Markup,
|
||||||
|
Go,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub board: Board,
|
pub board: Board,
|
||||||
pub modifiable: [[bool; 9]; 9],
|
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_row: usize,
|
||||||
pub cur_col: usize,
|
pub cur_col: usize,
|
||||||
}
|
}
|
||||||
@ -27,6 +39,10 @@ impl State {
|
|||||||
Self {
|
Self {
|
||||||
board,
|
board,
|
||||||
modifiable,
|
modifiable,
|
||||||
|
markups: [[[false; 9]; 9]; 9],
|
||||||
|
mode: Mode::default(),
|
||||||
|
next_mode: Mode::default(),
|
||||||
|
cur_num: 1,
|
||||||
cur_row: 4,
|
cur_row: 4,
|
||||||
cur_col: 4,
|
cur_col: 4,
|
||||||
}
|
}
|
||||||
@ -44,7 +60,7 @@ impl State {
|
|||||||
modifiable
|
modifiable
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_cell_modifiable(&self) -> bool {
|
pub fn current_cell_is_modifiable(&self) -> bool {
|
||||||
self.modifiable[self.cur_row][self.cur_col]
|
self.modifiable[self.cur_row][self.cur_col]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,4 +85,64 @@ impl State {
|
|||||||
_ => self.cur_row,
|
_ => 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,14 +5,16 @@ use crate::generator::Difficulty::*;
|
|||||||
use crate::rand::{seq::SliceRandom, thread_rng};
|
use crate::rand::{seq::SliceRandom, thread_rng};
|
||||||
use crate::sudoku::{Board, New};
|
use crate::sudoku::{Board, New};
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
/// categories of difficulty, indicating how many
|
/// categories of difficulty, indicating how many
|
||||||
/// empty spaces will be on a sudoku board.
|
/// empty spaces will be on a sudoku board.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub enum Difficulty {
|
pub enum Difficulty {
|
||||||
Easy,
|
Easy,
|
||||||
Medium,
|
Mid,
|
||||||
Hard,
|
Hard,
|
||||||
Extreme,
|
Expert,
|
||||||
Custom(usize),
|
Custom(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,14 +24,26 @@ impl Difficulty {
|
|||||||
pub fn removal_count(&self) -> usize {
|
pub fn removal_count(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Easy => 35,
|
Easy => 35,
|
||||||
Medium => 45,
|
Mid => 45,
|
||||||
Hard => 52,
|
Hard => 52,
|
||||||
Extreme => 62,
|
Expert => 62,
|
||||||
Custom(x) => *x,
|
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`.
|
/// generate a random, unsolved sudoku board with a given `Difficulty`.
|
||||||
pub fn generate_sudoku(difficulty: Difficulty) -> Board {
|
pub fn generate_sudoku(difficulty: Difficulty) -> Board {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
|
|||||||
13
src/ui.rs
13
src/ui.rs
@ -1,3 +1,4 @@
|
|||||||
|
use crate::state::*;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
@ -70,15 +71,13 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, board: [[u8; 9]; 9]) {
|
pub fn render(&mut self, state: &State) {
|
||||||
let mut lines = Vec::new();
|
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 {
|
match row {
|
||||||
3 | 6 => {
|
3 | 6 => lines.push("├────────┼────────┼────────┤".to_string()),
|
||||||
lines.push("├────────┼────────┼────────┤".to_string().chars().collect());
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
@ -97,7 +96,7 @@ where
|
|||||||
line.push_str(" │");
|
line.push_str(" │");
|
||||||
lines.push(line);
|
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_hori = self.width / 2 + lines[0].chars().count() / 2;
|
||||||
let pad_vert = self.height - lines.len();
|
let pad_vert = self.height - lines.len();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user