Compare commits
10 Commits
7594ac19d6
...
2352f3bfda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2352f3bfda | ||
|
|
0680baddd5 | ||
|
|
1084184a9b | ||
|
|
8f2c411975 | ||
|
|
6424063250 | ||
|
|
e8b47f31ac | ||
|
|
d5678967e3 | ||
|
|
c94ef2721d | ||
|
|
b2f2e0fad2 | ||
|
|
3e9b843131 |
12
README.md
12
README.md
@ -51,11 +51,10 @@ The current control scheme adheres to vim-like keybindings and is modal.
|
|||||||
|
|
||||||
### Todo
|
### Todo
|
||||||
|
|
||||||
- [ ] Game logic
|
- [x] Game logic
|
||||||
- [x] Validate Sudokus
|
- [x] Validate Sudokus
|
||||||
- [x] Generate Sudokus
|
- [x] Generate Sudokus
|
||||||
- [x] Difficulties to choose
|
- [x] Difficulties to choose
|
||||||
- [ ] Undo functionality
|
|
||||||
|
|
||||||
|
|
||||||
- [x] Basic UI
|
- [x] Basic UI
|
||||||
@ -70,12 +69,13 @@ The current control scheme adheres to vim-like keybindings and is modal.
|
|||||||
- [x] Preselect numbers
|
- [x] Preselect numbers
|
||||||
- [x] Edit Mode to (re)place numbers
|
- [x] Edit Mode to (re)place numbers
|
||||||
- [x] Markup Mode to mark where numbers could go
|
- [x] Markup Mode to mark where numbers could go
|
||||||
|
- [x] Autoremove marks after placing number in edit mode
|
||||||
- [x] Go Mode to move to blocks 1-9
|
- [x] Go Mode to move to blocks 1-9
|
||||||
- [x] Toggle Number/Mark with Space
|
- [x] Toggle Number/Mark with Space
|
||||||
- [ ] Undo/Redo stack
|
- [x] Undo/Redo stack
|
||||||
- [ ] Colored UI
|
- [x] Colored UI
|
||||||
- [ ] Hightlight preselected numbers
|
- [x] Hightlight preselected numbers
|
||||||
- [ ] Hightlight preselected markups
|
- [x] Hightlight preselected markups
|
||||||
- [x] Scoreboard
|
- [x] Scoreboard
|
||||||
- [x] Live timer
|
- [x] Live timer
|
||||||
- [x] Mode indicator
|
- [x] Mode indicator
|
||||||
|
|||||||
24
src/main.rs
24
src/main.rs
@ -4,19 +4,16 @@ use crossterm::event::{poll, read, Event::*, KeyCode::*};
|
|||||||
extern crate rand;
|
extern crate rand;
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
use state::*;
|
|
||||||
|
|
||||||
mod sudoku;
|
mod sudoku;
|
||||||
use sudoku::*;
|
|
||||||
|
|
||||||
mod ui;
|
mod ui;
|
||||||
use ui::*;
|
use {state::*, sudoku::*, ui::*};
|
||||||
|
|
||||||
use std::{io, time::Duration};
|
use std::{io, time::Duration};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut screen = Screen::init(io::stdout());
|
let mut screen = Ui::init(io::stdout());
|
||||||
let mut state = State::init(Difficulty::Mid);
|
let mut state = State::init(Difficulty::Mid);
|
||||||
|
screen.draw_static_elements().or_crash();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if poll(Duration::from_millis(250)).unwrap_or(false) {
|
if poll(Duration::from_millis(250)).unwrap_or(false) {
|
||||||
@ -49,7 +46,7 @@ fn main() {
|
|||||||
state.toggle_current_cell();
|
state.toggle_current_cell();
|
||||||
state.enter_next_mode();
|
state.enter_next_mode();
|
||||||
if is_solution(&state.board) {
|
if is_solution(&state.board) {
|
||||||
screen.deinit();
|
screen.deinit().or_crash();
|
||||||
println!("+------------+");
|
println!("+------------+");
|
||||||
println!("| You Win :) |");
|
println!("| You Win :) |");
|
||||||
println!("+------------+");
|
println!("+------------+");
|
||||||
@ -80,8 +77,11 @@ fn main() {
|
|||||||
_ => state.preselect_num(num as u8 - b'0'),
|
_ => state.preselect_num(num as u8 - b'0'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Char('u') | Char('U') => state.undo(),
|
||||||
|
Char('r') | Char('R') => state.redo(),
|
||||||
|
|
||||||
Char('q') | Char('Q') => {
|
Char('q') | Char('Q') => {
|
||||||
screen.deinit();
|
screen.deinit().or_crash();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,13 +91,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
screen.update_dimensions()
|
screen.draw(&state).or_crash();
|
||||||
.unwrap_or_else(|_| std::process::exit(1));
|
|
||||||
|
|
||||||
screen.render(&state);
|
|
||||||
|
|
||||||
screen.draw(state.cur_row, state.cur_col)
|
|
||||||
.unwrap_or_else(|_| std::process::exit(1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
346
src/state.rs
346
src/state.rs
@ -3,62 +3,57 @@ use crate::Dir::*;
|
|||||||
|
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
pub enum Dir {
|
/// the entire game logic state
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
FarUp,
|
|
||||||
FarDown,
|
|
||||||
FarLeft,
|
|
||||||
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 difficulty: Difficulty,
|
|
||||||
pub modifiable: [[bool; 9]; 9],
|
pub modifiable: [[bool; 9]; 9],
|
||||||
pub markups: [[[bool; 9]; 9]; 9],
|
pub markups: [[[bool; 9]; 9]; 9],
|
||||||
pub start_time: time::Instant,
|
|
||||||
pub mode: Mode,
|
|
||||||
pub next_mode: Mode,
|
|
||||||
pub preselection: u8,
|
pub preselection: u8,
|
||||||
pub cur_row: usize,
|
pub cur_row: usize,
|
||||||
pub cur_col: usize,
|
pub cur_col: usize,
|
||||||
|
|
||||||
|
pub mode: Mode,
|
||||||
|
pub next_mode: Mode,
|
||||||
|
|
||||||
|
pub difficulty: Difficulty,
|
||||||
|
pub start_time: time::Instant,
|
||||||
|
|
||||||
|
pub undo_stack: Vec<UndoStep>,
|
||||||
|
pub redo_stack: Vec<UndoStep>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
/// returns a new `State` with a randomly generated
|
||||||
|
/// sudoku `Board` of the provided `Difficulty`
|
||||||
pub fn init(difficulty: Difficulty) -> Self {
|
pub fn init(difficulty: Difficulty) -> Self {
|
||||||
let board = generate_sudoku(difficulty);
|
let board = generate_sudoku(difficulty);
|
||||||
let modifiable = State::get_modifiables(board);
|
let modifiable = State::init_modifiables(board);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
board,
|
board,
|
||||||
difficulty,
|
|
||||||
modifiable,
|
modifiable,
|
||||||
markups: [[[false; 9]; 9]; 9],
|
markups: [[[false; 9]; 9]; 9],
|
||||||
start_time: time::Instant::now(),
|
|
||||||
mode: Mode::default(),
|
|
||||||
next_mode: Mode::default(),
|
|
||||||
preselection: 1,
|
preselection: 1,
|
||||||
cur_row: 4,
|
cur_row: 4,
|
||||||
cur_col: 4,
|
cur_col: 4,
|
||||||
|
|
||||||
|
mode: Mode::default(),
|
||||||
|
next_mode: Mode::default(),
|
||||||
|
|
||||||
|
difficulty,
|
||||||
|
start_time: time::Instant::now(),
|
||||||
|
|
||||||
|
undo_stack: Vec::with_capacity(160),
|
||||||
|
redo_stack: Vec::with_capacity(80),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_time(&self) -> time::Duration {
|
/// returns a boolean mask of the board, indicating which cells
|
||||||
time::Instant::now() - self.start_time
|
/// can be modified by the user and which are part of the puzzle constraints.
|
||||||
}
|
/// makes only cells that are initialized with the value 0 modifiable
|
||||||
|
fn init_modifiables(board: Board) -> [[bool; 9]; 9] {
|
||||||
fn get_modifiables(board: Board) -> [[bool; 9]; 9] {
|
|
||||||
let mut modifiable = [[false; 9]; 9];
|
let mut modifiable = [[false; 9]; 9];
|
||||||
for (board_row, modifiable_row) in board.iter().zip(modifiable.iter_mut()) {
|
for (board_row, modifiable_row) in board.iter().zip(modifiable.iter_mut()) {
|
||||||
for (board_cell, modifiable_flag) in board_row.iter().zip(modifiable_row.iter_mut()) {
|
for (board_cell, modifiable_flag) in board_row.iter().zip(modifiable_row.iter_mut()) {
|
||||||
@ -74,6 +69,8 @@ impl State {
|
|||||||
self.modifiable[self.cur_row][self.cur_col]
|
self.modifiable[self.cur_row][self.cur_col]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns a mutable reference to the number
|
||||||
|
/// contained in the current cell.
|
||||||
pub fn current_cell(&mut self) -> &mut u8 {
|
pub fn current_cell(&mut self) -> &mut u8 {
|
||||||
&mut self.board[self.cur_row][self.cur_col]
|
&mut self.board[self.cur_row][self.cur_col]
|
||||||
}
|
}
|
||||||
@ -102,10 +99,11 @@ impl State {
|
|||||||
|
|
||||||
pub fn toggle_current_cell(&mut self) {
|
pub fn toggle_current_cell(&mut self) {
|
||||||
if self.current_cell_is_modifiable() {
|
if self.current_cell_is_modifiable() {
|
||||||
*self.current_cell() = if *self.current_cell() == self.preselection {
|
if *self.current_cell() == self.preselection {
|
||||||
0
|
self.delete_current_cell();
|
||||||
} else {
|
} else {
|
||||||
self.preselection
|
*self.current_cell() = self.preselection;
|
||||||
|
self.delete_colliding_marks(self.preselection, self.cur_row, self.cur_col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,9 +111,93 @@ impl State {
|
|||||||
pub fn delete_current_cell(&mut self) {
|
pub fn delete_current_cell(&mut self) {
|
||||||
if self.current_cell_is_modifiable() {
|
if self.current_cell_is_modifiable() {
|
||||||
*self.current_cell() = 0;
|
*self.current_cell() = 0;
|
||||||
|
|
||||||
|
self.push_to_undo_stack(UndoStep::Replace(
|
||||||
|
self.preselection,
|
||||||
|
(self.cur_row as u8, self.cur_col as u8),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// deletes all marks of `num` in it's row, column and block.
|
||||||
|
/// used to automatically remove marks when placing a number that
|
||||||
|
/// invalidates those marks.
|
||||||
|
pub fn delete_colliding_marks(&mut self, num: u8, row: usize, col: usize) {
|
||||||
|
let mut deleted = [None; 27];
|
||||||
|
let mut pos = 0;
|
||||||
|
|
||||||
|
for i in 0..9 {
|
||||||
|
if self.markups[i][col][num as usize - 1] {
|
||||||
|
deleted[pos] = Some((i as u8, col as u8));
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if self.markups[row][i][num as usize - 1] {
|
||||||
|
deleted[pos] = Some((row as u8, i as u8));
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if self.markups[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3][num as usize - 1] {
|
||||||
|
deleted[pos] = Some((
|
||||||
|
row as u8 / 3 * 3 + i as u8 / 3,
|
||||||
|
col as u8 / 3 * 3 + i as u8 % 3,
|
||||||
|
));
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.markups[i][col][num as usize - 1] = false;
|
||||||
|
self.markups[row][i][num as usize - 1] = false;
|
||||||
|
self.markups[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3][num as usize - 1] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.push_to_undo_stack(UndoStep::Unplace(
|
||||||
|
self.preselection,
|
||||||
|
(row as u8, col as u8),
|
||||||
|
deleted,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_current_mark(&mut self) {
|
||||||
|
if self.markups[self.cur_row][self.cur_col][self.preselection as usize - 1] {
|
||||||
|
self.delete_current_mark();
|
||||||
|
} else {
|
||||||
|
self.set_current_mark();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_current_mark(&mut self) {
|
||||||
|
if *self.current_cell() != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.markups[self.cur_row][self.cur_col][self.preselection as usize - 1] = false;
|
||||||
|
|
||||||
|
self.push_to_undo_stack(UndoStep::Remark(
|
||||||
|
self.preselection,
|
||||||
|
(self.cur_row as u8, self.cur_col as u8),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_mark(&mut self) {
|
||||||
|
if *self.current_cell() != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.markups[self.cur_row][self.cur_col][self.preselection as usize - 1] = true;
|
||||||
|
|
||||||
|
self.push_to_undo_stack(UndoStep::Unmark(
|
||||||
|
self.preselection,
|
||||||
|
(self.cur_row as u8, self.cur_col as u8),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 enter_mode(&mut self, mode: Mode) {
|
pub fn enter_mode(&mut self, mode: Mode) {
|
||||||
match mode {
|
match mode {
|
||||||
Mode::Go => {
|
Mode::Go => {
|
||||||
@ -128,6 +210,8 @@ impl State {
|
|||||||
self.next_mode = self.mode;
|
self.next_mode = self.mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// enter a mode until one action has been taken,
|
||||||
|
/// then returning to the previous mode
|
||||||
pub fn enter_mode_once(&mut self, mode: Mode) {
|
pub fn enter_mode_once(&mut self, mode: Mode) {
|
||||||
if self.mode != mode {
|
if self.mode != mode {
|
||||||
self.next_mode = self.mode;
|
self.next_mode = self.mode;
|
||||||
@ -139,24 +223,12 @@ impl State {
|
|||||||
self.mode = self.next_mode;
|
self.mode = self.next_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_cursor_to(&mut self, row: usize, col: usize) {
|
fn get_elapsed_time(&self) -> time::Duration {
|
||||||
assert!(
|
time::Instant::now() - self.start_time
|
||||||
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) {
|
/// returns number of filled cells
|
||||||
self.markups[self.cur_row][self.cur_col][self.preselection as usize] ^= true;
|
pub fn get_completion_string(&self) -> String {
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_current_mark(&mut self) {
|
|
||||||
self.markups[self.cur_row][self.cur_col][self.preselection as usize] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_completion_chars(&self) -> [char; 2] {
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for row in self.board {
|
for row in self.board {
|
||||||
for cell in row {
|
for cell in row {
|
||||||
@ -166,9 +238,12 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let to_char = |x| (x + b'0') as char;
|
let to_char = |x| (x + b'0') as char;
|
||||||
[to_char(count / 10), to_char(count % 10)]
|
[to_char(count / 10), to_char(count % 10)].iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns how many of the 9 final occurences of the
|
||||||
|
/// preselected number have been found.
|
||||||
|
/// returns `'!'` if the user erroneously placed more than 9.
|
||||||
pub fn get_preselection_completion_char(&self) -> char {
|
pub fn get_preselection_completion_char(&self) -> char {
|
||||||
let count = self
|
let count = self
|
||||||
.board
|
.board
|
||||||
@ -176,40 +251,147 @@ impl State {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.filter(|&cell| cell == self.preselection)
|
.filter(|&cell| cell == self.preselection)
|
||||||
.count();
|
.count();
|
||||||
(count.min(9) as u8 + b'0') as char
|
match count {
|
||||||
}
|
10.. => '!',
|
||||||
|
_ => (count as u8 + b'0') as char,
|
||||||
pub fn get_difficulty_chars(&self) -> [char; 5] {
|
|
||||||
match self.difficulty {
|
|
||||||
Difficulty::Easy => [' ', 'E', 'a', 's', 'y'],
|
|
||||||
Difficulty::Mid => [' ', 'M', 'i', 'd', ' '],
|
|
||||||
Difficulty::Hard => [' ', 'H', 'a', 'r', 'd'],
|
|
||||||
Difficulty::Expert => ['E', 'x', 'p', 'r', 't'],
|
|
||||||
Difficulty::Custom(x) => [
|
|
||||||
'C',
|
|
||||||
'(',
|
|
||||||
(x as u8 / 10 + b'0') as char,
|
|
||||||
(x as u8 % 10 + b'0') as char,
|
|
||||||
')',
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_timer_chars(&self) -> [char; 5] {
|
/// returns the difficulty string used on the ingame scoreboard.
|
||||||
let mut secs = self.get_time().as_secs();
|
/// if you want the complete difficulty names use `Difficulty::to_string()`
|
||||||
let mut chars = [':'; 5];
|
pub fn get_difficulty_string(&self) -> String {
|
||||||
let to_char = |x: u64| (x as u8 + b'0') as char;
|
match self.difficulty {
|
||||||
chars[0] = to_char(secs / 600);
|
Difficulty::Easy => String::from("Easy "),
|
||||||
secs %= 600;
|
Difficulty::Mid => String::from(" Mid "),
|
||||||
chars[1] = to_char(secs / 60);
|
Difficulty::Hard => String::from("Hard "),
|
||||||
secs %= 60;
|
Difficulty::Expert => String::from("Exprt"),
|
||||||
chars[3] = to_char(secs / 10);
|
Difficulty::Custom(x) => format!("C({:02})", x),
|
||||||
secs %= 10;
|
}
|
||||||
chars[4] = to_char(secs);
|
|
||||||
chars
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns (mins, secs) as a pair of strings
|
||||||
|
/// both are zero-padded to a width of 2 characters
|
||||||
|
pub fn get_timer_strings(&self) -> (String, String) {
|
||||||
|
let mut n = self.get_elapsed_time().as_secs();
|
||||||
|
|
||||||
|
let mut mins = String::with_capacity(2);
|
||||||
|
let mut secs = String::with_capacity(2);
|
||||||
|
|
||||||
|
let to_char = |x: u64| (x as u8 + b'0') as char;
|
||||||
|
|
||||||
|
mins.push(to_char(n / 600));
|
||||||
|
n %= 600;
|
||||||
|
mins.push(to_char(n / 60));
|
||||||
|
n %= 60;
|
||||||
|
|
||||||
|
secs.push(to_char(n / 10));
|
||||||
|
n %= 10;
|
||||||
|
secs.push(to_char(n));
|
||||||
|
|
||||||
|
(mins, secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns timer as string in `mm:ss` format
|
||||||
pub fn get_timer_string(&self) -> String {
|
pub fn get_timer_string(&self) -> String {
|
||||||
self.get_timer_chars().iter().collect()
|
let (mins, secs) = self.get_timer_strings();
|
||||||
|
let mut timer = String::with_capacity(5);
|
||||||
|
timer.push_str(&mins);
|
||||||
|
timer.push(':');
|
||||||
|
timer.push_str(&secs);
|
||||||
|
timer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// undo an action that has been taken
|
||||||
|
pub fn undo(&mut self) {
|
||||||
|
use UndoStep::*;
|
||||||
|
|
||||||
|
if let Some(undo_step) = self.undo_stack.pop() {
|
||||||
|
self.redo_stack.push(undo_step);
|
||||||
|
match undo_step {
|
||||||
|
Unplace(num, (row, col), deleted_marks) => {
|
||||||
|
self.board[row as usize][col as usize] = 0;
|
||||||
|
for (r, c) in deleted_marks.iter().take_while(|x| x.is_some()).flatten() {
|
||||||
|
self.markups[*r as usize][*c as usize][num as usize - 1] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Replace(num, (row, col)) => {
|
||||||
|
self.board[row as usize][col as usize] = num;
|
||||||
|
}
|
||||||
|
Remark(num, (row, col)) => {
|
||||||
|
self.markups[row as usize][col as usize][num as usize - 1] = true;
|
||||||
|
}
|
||||||
|
Unmark(num, (row, col)) => {
|
||||||
|
self.markups[row as usize][col as usize][num as usize - 1] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// redo an action if one was taken and undone.
|
||||||
|
pub fn redo(&mut self) {
|
||||||
|
use UndoStep::*;
|
||||||
|
|
||||||
|
if let Some(redo_step) = self.redo_stack.pop() {
|
||||||
|
self.undo_stack.push(redo_step);
|
||||||
|
match redo_step {
|
||||||
|
Unplace(num, (row, col), deleted_marks) => {
|
||||||
|
self.board[row as usize][col as usize] = num;
|
||||||
|
for (r, c) in deleted_marks.iter().take_while(|x| x.is_some()).flatten() {
|
||||||
|
self.markups[*r as usize][*c as usize][num as usize - 1] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Replace(_, (row, col)) => {
|
||||||
|
self.board[row as usize][col as usize] = 0;
|
||||||
|
}
|
||||||
|
Remark(num, (row, col)) => {
|
||||||
|
self.markups[row as usize][col as usize][num as usize - 1] = false;
|
||||||
|
}
|
||||||
|
Unmark(num, (row, col)) => {
|
||||||
|
self.markups[row as usize][col as usize][num as usize - 1] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// pushes a move done by the player to the undo stack
|
||||||
|
/// this additionally invalidates the redo stack
|
||||||
|
pub fn push_to_undo_stack(&mut self, undo_step: UndoStep) {
|
||||||
|
self.undo_stack.push(undo_step);
|
||||||
|
self.redo_stack.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Dir {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
FarUp,
|
||||||
|
FarDown,
|
||||||
|
FarLeft,
|
||||||
|
FarRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum Mode {
|
||||||
|
#[default]
|
||||||
|
Edit,
|
||||||
|
Markup,
|
||||||
|
Go,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum UndoStep {
|
||||||
|
/// unplace num at (row, col) and re-mark
|
||||||
|
/// (row, col) if there were removed marks
|
||||||
|
Unplace(u8, (u8, u8), [Option<(u8, u8)>; 27]),
|
||||||
|
|
||||||
|
/// re-place num at (row, col)
|
||||||
|
Replace(u8, (u8, u8)),
|
||||||
|
|
||||||
|
/// unmark num at (row, col)
|
||||||
|
Unmark(u8, (u8, u8)),
|
||||||
|
|
||||||
|
/// re-mark num at (row, col)
|
||||||
|
Remark(u8, (u8, u8)),
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
// TODO: remove when done
|
|
||||||
#![cfg_attr(debug_assertions, allow(unused))]
|
|
||||||
|
|
||||||
use crate::generator::Difficulty::*;
|
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};
|
||||||
@ -9,6 +6,7 @@ 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.
|
||||||
|
/// (see `Difficulty::removal_count()` for values)
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Default, Copy, Clone, PartialEq)]
|
#[derive(Default, Copy, Clone, PartialEq)]
|
||||||
pub enum Difficulty {
|
pub enum Difficulty {
|
||||||
@ -25,10 +23,10 @@ impl Difficulty {
|
|||||||
/// sudoku board of the given `Difficulty`
|
/// sudoku board of the given `Difficulty`
|
||||||
pub fn removal_count(&self) -> usize {
|
pub fn removal_count(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Easy => 35,
|
Easy => 31,
|
||||||
Mid => 45,
|
Mid => 39,
|
||||||
Hard => 52,
|
Hard => 49,
|
||||||
Expert => 62,
|
Expert => 61,
|
||||||
Custom(x) => *x,
|
Custom(x) => *x,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,6 +45,8 @@ impl fmt::Display for Difficulty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// generate a random, unsolved sudoku board with a given `Difficulty`.
|
/// generate a random, unsolved sudoku board with a given `Difficulty`.
|
||||||
|
/// solves a empty sudoku with random cell order and then removes
|
||||||
|
/// some number of cells depending on the 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();
|
||||||
|
|
||||||
@ -65,13 +65,15 @@ pub fn generate_sudoku(difficulty: Difficulty) -> Board {
|
|||||||
board
|
board
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// solves a sudoku, randomizing which empty cell of equal
|
||||||
|
/// mrv to choose next.
|
||||||
|
/// this is used to generate random, fully solvable sudokus.
|
||||||
|
///
|
||||||
|
/// NOTE: this does not guarantee a single-solution sudoku.
|
||||||
|
///
|
||||||
/// TODO: currently empty cells needs to be recreated
|
/// TODO: currently empty cells needs to be recreated
|
||||||
/// on each recursive call, and are randomized anew
|
/// on each recursive call, and are randomized anew
|
||||||
/// each time, which is an unnecessary overhead.
|
/// each time, which is an unnecessary overhead.
|
||||||
///
|
|
||||||
/// solves a sudoku, randomizing which empty cell of equal
|
|
||||||
/// mrv () to choose next.
|
|
||||||
/// this is used to generate random, fully solvable sudokus.
|
|
||||||
fn solve_random(board: &mut Board) -> Result<(), ()> {
|
fn solve_random(board: &mut Board) -> Result<(), ()> {
|
||||||
let mut empty_cells: Vec<(usize, usize)> = Vec::new();
|
let mut empty_cells: Vec<(usize, usize)> = Vec::new();
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ trait New {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl New for Board {
|
impl New for Board {
|
||||||
|
/// return an empty `Board`
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
[[0u8; 9]; 9]
|
[[0u8; 9]; 9]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
// TODO: remove when done
|
|
||||||
#![cfg_attr(debug_assertions, allow(unused))]
|
|
||||||
|
|
||||||
use crate::sudoku::Board;
|
use crate::sudoku::Board;
|
||||||
|
|
||||||
|
/// returns true if the provided sudoku `Board` is in a solved state
|
||||||
pub fn is_solution(sudoku: &Board) -> bool {
|
pub fn is_solution(sudoku: &Board) -> bool {
|
||||||
for i in 0..9 {
|
for i in 0..9 {
|
||||||
let mut row_set = 0u16;
|
let mut row_set = 0u16;
|
||||||
|
|||||||
414
src/ui.rs
414
src/ui.rs
@ -1,65 +1,95 @@
|
|||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
|
use std::cmp::Ordering::*;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::{MoveTo, MoveToColumn, SetCursorStyle},
|
cursor::{
|
||||||
|
MoveDown, MoveLeft, MoveRight, MoveTo, MoveToColumn, MoveUp, RestorePosition, SavePosition,
|
||||||
|
SetCursorStyle,
|
||||||
|
},
|
||||||
execute, queue,
|
execute, queue,
|
||||||
|
style::{Color, SetBackgroundColor, SetForegroundColor},
|
||||||
terminal::{
|
terminal::{
|
||||||
disable_raw_mode, enable_raw_mode, size, Clear, ClearType::All, EnterAlternateScreen,
|
disable_raw_mode, enable_raw_mode, size, Clear, ClearType::All, EnterAlternateScreen,
|
||||||
LeaveAlternateScreen,
|
LeaveAlternateScreen,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Screen<T>
|
/// state of the user interface
|
||||||
|
pub struct Ui<T>
|
||||||
where
|
where
|
||||||
T: io::Write,
|
T: io::Write,
|
||||||
{
|
{
|
||||||
pub data: Vec<Vec<char>>,
|
pub presel_color_pair: (Color, Color),
|
||||||
|
pub markup_color_background: Color,
|
||||||
pub ostream: T,
|
pub ostream: T,
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Screen<T>
|
impl<T> Ui<T>
|
||||||
where
|
where
|
||||||
T: io::Write,
|
T: io::Write,
|
||||||
{
|
{
|
||||||
|
/// returns a new `Ui` using `ostream` as its' output stream
|
||||||
|
/// and sets up terminal state:
|
||||||
|
/// - remembers the current cursor position
|
||||||
|
/// - enters alternate screen buffer
|
||||||
|
/// - enables raw mode
|
||||||
pub fn init(ostream: T) -> Self {
|
pub fn init(ostream: T) -> Self {
|
||||||
let (width, height) = size().unwrap();
|
let (width, height) = size().unwrap();
|
||||||
let (width, height) = (width as usize, height as usize);
|
let (width, height) = (width as usize, height as usize);
|
||||||
|
|
||||||
let data = RENDER_TEMPLATE.iter().map(|s| s.to_vec()).collect();
|
let presel_color_pair = (Color::Black, Color::Cyan);
|
||||||
|
let markup_color_background = Color::Cyan;
|
||||||
|
|
||||||
let mut screen = Screen {
|
let mut ui = Ui {
|
||||||
data,
|
presel_color_pair,
|
||||||
|
markup_color_background,
|
||||||
ostream,
|
ostream,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
};
|
};
|
||||||
|
|
||||||
enable_raw_mode().expect("[-]: Error: ui::init: Failed to enable raw mode.");
|
queue!(ui.ostream, SavePosition, EnterAlternateScreen, Clear(All))
|
||||||
queue!(screen.ostream, EnterAlternateScreen)
|
|
||||||
.expect("[-]: Error: ui::init: Failed to enter alternate screen.");
|
.expect("[-]: Error: ui::init: Failed to enter alternate screen.");
|
||||||
screen
|
enable_raw_mode().expect("[-]: Error: ui::init: Failed to enable raw mode.");
|
||||||
|
ui
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(&mut self) {
|
/// resets terminal state that `Ui::init()` sets:
|
||||||
disable_raw_mode().unwrap_or(());
|
/// - disables raw mode
|
||||||
execute!(self.ostream, LeaveAlternateScreen).unwrap_or(());
|
/// - leaves alternate screen buffer
|
||||||
|
/// - restores cursor position
|
||||||
|
pub fn deinit(&mut self) -> io::Result<()> {
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(self.ostream, LeaveAlternateScreen, RestorePosition)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// updates `width` and `height`.
|
||||||
|
/// clears the screen and redraws the board if the dimensions changed.
|
||||||
|
/// returns an error if the new dimensions are too small to fit the ui.
|
||||||
pub fn update_dimensions(&mut self) -> io::Result<()> {
|
pub fn update_dimensions(&mut self) -> io::Result<()> {
|
||||||
|
let old_dimensions = (self.width, self.height);
|
||||||
|
|
||||||
let (width, height) = size()?;
|
let (width, height) = size()?;
|
||||||
self.width = width as usize;
|
self.width = width as usize;
|
||||||
self.height = height as usize;
|
self.height = height as usize;
|
||||||
|
|
||||||
if height < 14 || width < 54 {
|
if height < 14 || width < 54 {
|
||||||
self.clear()?;
|
self.clear()?;
|
||||||
self.deinit();
|
self.deinit()?;
|
||||||
eprintln!("[-]: Error: ui::update_dimensions: Terminal size too small to display UI.");
|
eprintln!("[!]: Error: ui::update_dimensions: Terminal size too small to display UI.");
|
||||||
return Err(io::Error::from(io::ErrorKind::Other));
|
return Err(io::Error::from(io::ErrorKind::Other));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if old_dimensions != (self.width, self.height) {
|
||||||
|
self.clear()?;
|
||||||
|
self.draw_static_elements()?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,105 +98,171 @@ where
|
|||||||
queue!(self.ostream, MoveToColumn(0))
|
queue!(self.ostream, MoveToColumn(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self, row: usize, col: usize) -> io::Result<()> {
|
/// draws the entire ui, watching for possibly changed screen dimensions.
|
||||||
self.draw_screen()?;
|
/// all other `draw_...()` functions just `queue!(...)` the actions to draw
|
||||||
self.draw_cursor(row, col)?;
|
/// their respective ui element.
|
||||||
self.ostream.flush()?;
|
/// this function chains all of these calls in order and then flushes the
|
||||||
|
/// changes to `self.ostream`.
|
||||||
|
///
|
||||||
|
/// NOTE: this function does not draw the static elements of the ui
|
||||||
|
/// using `Ui::draw_static_elements()`. for efficiency these are only
|
||||||
|
/// redrawn on screen dimensions changes and initialization.
|
||||||
|
pub fn draw(&mut self, state: &State) -> io::Result<()> {
|
||||||
|
self.update_dimensions()?;
|
||||||
|
|
||||||
|
self.draw_numbers(state)?;
|
||||||
|
self.draw_scoreboard(state)?;
|
||||||
|
self.draw_cursor(state)?;
|
||||||
|
|
||||||
|
self.ostream.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `queue!(...)`s the drawing of the unchanging ui elements
|
||||||
|
/// such as the board and scoreboards outlines.
|
||||||
|
/// uses the lines provided by `board_template()`.
|
||||||
|
///
|
||||||
|
/// NOTE: this function itself does not flush to `self.ostream`
|
||||||
|
/// in order to only have to flush once per frame.
|
||||||
|
pub fn draw_static_elements(&mut self) -> io::Result<()> {
|
||||||
|
self.init_cursor_offset()?;
|
||||||
|
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
|
||||||
|
queue!(self.ostream, SetBackgroundColor(Color::Reset))?;
|
||||||
|
|
||||||
|
let board_template = board_template();
|
||||||
|
|
||||||
|
for line in board_template {
|
||||||
|
write!(self.ostream, "{}", line)?;
|
||||||
|
self.move_cursor_by(-41, 1)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_screen(&mut self) -> io::Result<()> {
|
/// `queue!(...)`s the drawing of the numbers in the cells.
|
||||||
self.clear()?;
|
///
|
||||||
let lft_pad = self.width / 2 - 14;
|
/// NOTE: this function itself does not flush to `self.ostream`
|
||||||
let bot_pad = self.height / 2 - 7;
|
/// in order to only have to flush once per frame.
|
||||||
|
fn draw_numbers(&mut self, state: &State) -> io::Result<()> {
|
||||||
|
self.init_cursor_offset()?;
|
||||||
|
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
|
||||||
|
queue!(self.ostream, SetBackgroundColor(Color::Reset))?;
|
||||||
|
self.move_cursor_by(1, 0)?;
|
||||||
|
|
||||||
let mut display = String::with_capacity((43 + lft_pad) * 13 + bot_pad * 2);
|
|
||||||
|
|
||||||
display.extend(self.data.iter().flat_map(|line| {
|
|
||||||
std::iter::repeat(' ')
|
|
||||||
.take(lft_pad)
|
|
||||||
.chain(line.to_owned())
|
|
||||||
.chain(['\r', '\n'])
|
|
||||||
}));
|
|
||||||
|
|
||||||
display.push_str(&"\r\n".repeat(bot_pad));
|
|
||||||
|
|
||||||
write!(self.ostream, "{}", display)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(&mut self, state: &State) {
|
|
||||||
self.render_board();
|
|
||||||
self.render_board_cells(state);
|
|
||||||
self.render_scoreboard_elements(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_board(&mut self) {
|
|
||||||
self.data = RENDER_TEMPLATE.iter().map(|s| s.to_vec()).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_board_cells(&mut self, state: &State) {
|
|
||||||
for row in 0..9 {
|
for row in 0..9 {
|
||||||
|
match row {
|
||||||
|
3 | 6 => self.move_cursor_by(0, 2),
|
||||||
|
_ => self.move_cursor_by(0, 1),
|
||||||
|
}?;
|
||||||
|
|
||||||
for col in 0..9 {
|
for col in 0..9 {
|
||||||
let i = row + 1 + row / 3;
|
match col {
|
||||||
let j = col + 2 + col + col / 3 * 3;
|
3 | 6 => self.move_cursor_by(4, 0),
|
||||||
|
_ => self.move_cursor_by(1, 0),
|
||||||
|
}?;
|
||||||
|
|
||||||
self.data[i][j] = match state.board[row][col] {
|
let chr = match state.board[row][col] {
|
||||||
0 => ' ',
|
x if x == state.preselection => {
|
||||||
x => (x + b'0') as char,
|
queue!(
|
||||||
}
|
self.ostream,
|
||||||
|
SetForegroundColor(self.presel_color_pair.0),
|
||||||
|
SetBackgroundColor(self.presel_color_pair.1)
|
||||||
|
)
|
||||||
|
.unwrap_or(());
|
||||||
|
(x + b'0') as char
|
||||||
|
}
|
||||||
|
0 => {
|
||||||
|
match state.markups[row][col][state.preselection as usize - 1] {
|
||||||
|
true => {
|
||||||
|
queue!(
|
||||||
|
self.ostream,
|
||||||
|
SetBackgroundColor(self.markup_color_background)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
queue!(
|
||||||
|
self.ostream,
|
||||||
|
SetForegroundColor(Color::Reset),
|
||||||
|
SetBackgroundColor(Color::Reset)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' '
|
||||||
|
}
|
||||||
|
x => {
|
||||||
|
queue!(self.ostream, SetForegroundColor(Color::Reset)).unwrap_or(());
|
||||||
|
queue!(self.ostream, SetBackgroundColor(Color::Reset)).unwrap_or(());
|
||||||
|
(x + b'0') as char
|
||||||
|
}
|
||||||
|
};
|
||||||
|
write!(self.ostream, "{}", chr)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.move_cursor_by(-24, 0)?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_scoreboard_elements(&mut self, state: &State) {
|
/// `queue!(...)`s the drawing of variable scoreboard content
|
||||||
// char indeces of scoreboard elements
|
///
|
||||||
// -----
|
/// NOTE: this function itself does not flush to `self.ostream`
|
||||||
//
|
/// in order to only have to flush once per frame.
|
||||||
// difficulty [1][34] - [1][38]
|
fn draw_scoreboard(&mut self, state: &State) -> io::Result<()> {
|
||||||
// completion [2][34] - [1][35]
|
self.init_cursor_offset()?;
|
||||||
//
|
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
|
||||||
// timer [4][34] - [3][38]
|
queue!(self.ostream, SetBackgroundColor(Color::Reset))?;
|
||||||
//
|
|
||||||
// > edit: [6][33]
|
|
||||||
// > markup: [7][33]
|
|
||||||
// > go: [8][33]
|
|
||||||
//
|
|
||||||
// preselection: [10][36]
|
|
||||||
// preselection completion: [11][34]
|
|
||||||
|
|
||||||
for (i, c) in (34..39).zip(state.get_difficulty_chars()) {
|
self.move_cursor_by(34, 1)?;
|
||||||
self.data[1][i] = c;
|
write!(self.ostream, "{}", state.get_difficulty_string())?;
|
||||||
|
|
||||||
|
self.move_cursor_by(-5, 1)?;
|
||||||
|
queue!(self.ostream, SetForegroundColor(self.presel_color_pair.1))?;
|
||||||
|
write!(self.ostream, "{}", state.get_completion_string())?;
|
||||||
|
|
||||||
|
self.move_cursor_by(-2, 2)?;
|
||||||
|
let (mins_string, secs_string) = state.get_timer_strings();
|
||||||
|
write!(self.ostream, "{}", mins_string)?;
|
||||||
|
self.move_cursor_by(1, 0)?;
|
||||||
|
write!(self.ostream, "{}", secs_string)?;
|
||||||
|
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
|
||||||
|
|
||||||
|
self.move_cursor_by(-6, 2)?;
|
||||||
|
for _ in 0..3 {
|
||||||
|
write!(self.ostream, " ")?;
|
||||||
|
self.move_cursor_by(-1, 1)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i, c) in (34..36).zip(state.get_completion_chars()) {
|
self.move_cursor_by(0, -3)?;
|
||||||
self.data[2][i] = c;
|
|
||||||
|
let selected_mode_idx = match state.mode {
|
||||||
|
Mode::Edit => 0,
|
||||||
|
Mode::Markup => 1,
|
||||||
|
Mode::Go => 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
queue!(self.ostream, SetForegroundColor(self.presel_color_pair.1))?;
|
||||||
|
for i in 0..3 {
|
||||||
|
if i == selected_mode_idx {
|
||||||
|
write!(self.ostream, ">")?;
|
||||||
|
}
|
||||||
|
self.move_cursor_by(0, 1)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i, c) in (34..39).zip(state.get_timer_chars()) {
|
self.move_cursor_by(2, 1)?;
|
||||||
self.data[4][i] = c;
|
write!(self.ostream, "{}", (state.preselection + b'0') as char)?;
|
||||||
}
|
|
||||||
|
|
||||||
for i in 6..9 {
|
self.move_cursor_by(-3, 1)?;
|
||||||
self.data[i][33] = ' ';
|
write!(self.ostream, "{}", state.get_preselection_completion_char())?;
|
||||||
}
|
|
||||||
|
|
||||||
self.data[match state.mode {
|
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
|
||||||
Mode::Edit => 6,
|
Ok(())
|
||||||
Mode::Markup => 7,
|
|
||||||
Mode::Go => 8,
|
|
||||||
}][33] = '>';
|
|
||||||
|
|
||||||
self.data[10][36] = (state.preselection + b'0') as char;
|
|
||||||
|
|
||||||
self.data[11][34] = state.get_preselection_completion_char();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_cursor(&mut self, row: usize, col: usize) -> io::Result<()> {
|
/// `queue!(...)`s the placement of the cursor on the selected cell.
|
||||||
let (x, y) = (
|
///
|
||||||
(self.width / 2 - 14) as u16,
|
/// NOTE: this function itself does not flush to `self.ostream`
|
||||||
(self.height / 2 - 7 + (self.height & 1)) as u16,
|
/// in order to only have to flush once per frame.
|
||||||
);
|
fn draw_cursor(&mut self, state: &State) -> io::Result<()> {
|
||||||
|
let (row, col) = (state.cur_row, state.cur_col);
|
||||||
|
let (x, y) = ((self.width / 2 - 14) as u16, (self.height / 2 - 6) as u16);
|
||||||
let (x, y) = (x + 2, y + 1);
|
let (x, y) = (x + 2, y + 1);
|
||||||
let (x, y) = (x + 2 * col as u16, y + row as u16);
|
let (x, y) = (x + 2 * col as u16, y + row as u16);
|
||||||
let x = match col {
|
let x = match col {
|
||||||
@ -181,72 +277,70 @@ where
|
|||||||
};
|
};
|
||||||
queue!(self.ostream, MoveTo(x, y), SetCursorStyle::SteadyBlock)
|
queue!(self.ostream, MoveTo(x, y), SetCursorStyle::SteadyBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `queue!(...)`s the movement of the cursor by (`x`, `y`).
|
||||||
|
/// positive `x` values correspond to movement right by `x` columns.
|
||||||
|
/// positive `y` values correspond to movement down by `y` rows.
|
||||||
|
///
|
||||||
|
/// NOTE: this function itself does not flush to `self.ostream`
|
||||||
|
/// in order to only have to flush once per frame.
|
||||||
|
fn move_cursor_by(&mut self, x: isize, y: isize) -> io::Result<()> {
|
||||||
|
match x.cmp(&0) {
|
||||||
|
Equal => Ok(()),
|
||||||
|
Less => queue!(self.ostream, MoveLeft(-x as u16)),
|
||||||
|
Greater => queue!(self.ostream, MoveRight(x as u16)),
|
||||||
|
}?;
|
||||||
|
match y.cmp(&0) {
|
||||||
|
Equal => Ok(()),
|
||||||
|
Less => queue!(self.ostream, MoveUp(-y as u16)),
|
||||||
|
Greater => queue!(self.ostream, MoveDown(y as u16)),
|
||||||
|
}?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `queue!(...)`s the movement of the cursor to the top-left
|
||||||
|
/// of the inner ui.
|
||||||
|
/// this simplifies handling the padding in other drawing functions
|
||||||
|
/// since the padding depends on the screen dimensions.
|
||||||
|
fn init_cursor_offset(&mut self) -> io::Result<()> {
|
||||||
|
let lft_pad = (self.width / 2 - 14) as u16;
|
||||||
|
let top_pad = (self.height / 2 - 6) as u16;
|
||||||
|
queue!(self.ostream, MoveTo(lft_pad, top_pad))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RENDER_TEMPLATE: [[char; 41]; 13] = [
|
/// returns a template for the parts of the board that
|
||||||
|
/// are always the same.
|
||||||
|
fn board_template() -> [String; 13] {
|
||||||
[
|
[
|
||||||
'┌', '─', '─', '─', '─', '─', '─', '─', '─', '┬', '─', '─', '─', '─', '─', '─', '─', '─',
|
String::from("┌────────┬────────┬────────┐ ┌───────┐"),
|
||||||
'┬', '─', '─', '─', '─', '─', '─', '─', '─', '┐', ' ', ' ', ' ', ' ', '┌', '─', '─', '─',
|
String::from("│ │ │ │ │ │"),
|
||||||
'─', '─', '─', '─', '┐',
|
String::from("│ │ │ │ │ /81 │"),
|
||||||
],
|
String::from("│ │ │ │ ├───────┤"),
|
||||||
[
|
String::from("├────────┼────────┼────────┤ │ : │"),
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
String::from("│ │ │ │ ├───────┤"),
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ',
|
String::from("│ │ │ │ │ Edit │"),
|
||||||
' ', ' ', ' ', ' ', '│',
|
String::from("│ │ │ │ │ Mark │"),
|
||||||
],
|
String::from("├────────┼────────┼────────┤ │ Go │"),
|
||||||
[
|
String::from("│ │ │ │ ├───────┤"),
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
String::from("│ │ │ │ │ [ ] │"),
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ',
|
String::from("│ │ │ │ │ / 9 │"),
|
||||||
'/', '8', '1', ' ', '│',
|
String::from("└────────┴────────┴────────┘ └───────┘"),
|
||||||
],
|
]
|
||||||
[
|
}
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─',
|
pub trait UiCrash {
|
||||||
'─', '─', '─', '─', '┤',
|
fn or_crash(&self);
|
||||||
],
|
}
|
||||||
[
|
|
||||||
'├', '─', '─', '─', '─', '─', '─', '─', '─', '┼', '─', '─', '─', '─', '─', '─', '─', '─',
|
impl<T> UiCrash for io::Result<T> {
|
||||||
'┼', '─', '─', '─', '─', '─', '─', '─', '─', '┤', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ',
|
/// ends the process with exit code 1
|
||||||
' ', ' ', ' ', ' ', '│',
|
/// if called on `Err(_)` variant.
|
||||||
],
|
/// if called on `Ok(_)` variant, this is a no-op
|
||||||
[
|
/// that discards the contained value.
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
fn or_crash(&self) {
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─',
|
if self.is_err() {
|
||||||
'─', '─', '─', '─', '┤',
|
std::process::exit(1);
|
||||||
],
|
}
|
||||||
[
|
}
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
}
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'E',
|
|
||||||
'd', 'i', 't', ' ', '│',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'M',
|
|
||||||
'a', 'r', 'k', ' ', '│',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'├', '─', '─', '─', '─', '─', '─', '─', '─', '┼', '─', '─', '─', '─', '─', '─', '─', '─',
|
|
||||||
'┼', '─', '─', '─', '─', '─', '─', '─', '─', '┤', ' ', ' ', ' ', ' ', '│', ' ', ' ', 'G',
|
|
||||||
'o', ' ', ' ', ' ', '│',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '├', '─', '─', '─',
|
|
||||||
'─', '─', '─', '─', '┤',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', '[',
|
|
||||||
' ', ']', ' ', ' ', '│',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
|
|
||||||
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ',
|
|
||||||
'/', ' ', '9', ' ', '│',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'└', '─', '─', '─', '─', '─', '─', '─', '─', '┴', '─', '─', '─', '─', '─', '─', '─', '─',
|
|
||||||
'┴', '─', '─', '─', '─', '─', '─', '─', '─', '┘', ' ', ' ', ' ', ' ', '└', '─', '─', '─',
|
|
||||||
'─', '─', '─', '─', '┘',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user