Compare commits

..

No commits in common. "2352f3bfda9adc1cd471208cd840aadd11b0b306" and "7594ac19d67f71f9cd3b97bc1de045dd45b83503" have entirely different histories.

7 changed files with 273 additions and 544 deletions

View File

@ -51,10 +51,11 @@ The current control scheme adheres to vim-like keybindings and is modal.
### Todo ### Todo
- [x] Game logic - [ ] 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
@ -69,13 +70,12 @@ 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
- [x] Undo/Redo stack - [ ] Undo/Redo stack
- [x] Colored UI - [ ] Colored UI
- [x] Hightlight preselected numbers - [ ] Hightlight preselected numbers
- [x] Hightlight preselected markups - [ ] Hightlight preselected markups
- [x] Scoreboard - [x] Scoreboard
- [x] Live timer - [x] Live timer
- [x] Mode indicator - [x] Mode indicator

View File

@ -4,16 +4,19 @@ 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 {state::*, sudoku::*, ui::*}; use ui::*;
use std::{io, time::Duration}; use std::{io, time::Duration};
fn main() { fn main() {
let mut screen = Ui::init(io::stdout()); let mut screen = Screen::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) {
@ -46,7 +49,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().or_crash(); screen.deinit();
println!("+------------+"); println!("+------------+");
println!("| You Win :) |"); println!("| You Win :) |");
println!("+------------+"); println!("+------------+");
@ -77,11 +80,8 @@ 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().or_crash(); screen.deinit();
break; break;
} }
@ -91,7 +91,13 @@ fn main() {
} }
} }
screen.draw(&state).or_crash(); screen.update_dimensions()
.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));
} }
} }

View File

@ -3,57 +3,62 @@ use crate::Dir::*;
use std::time; use std::time;
/// the entire game logic state pub enum Dir {
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::init_modifiables(board); let modifiable = State::get_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),
} }
} }
/// returns a boolean mask of the board, indicating which cells fn get_time(&self) -> time::Duration {
/// can be modified by the user and which are part of the puzzle constraints. time::Instant::now() - self.start_time
/// 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()) {
@ -69,8 +74,6 @@ 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]
} }
@ -99,11 +102,10 @@ 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() {
if *self.current_cell() == self.preselection { *self.current_cell() = if *self.current_cell() == self.preselection {
self.delete_current_cell(); 0
} else { } else {
*self.current_cell() = self.preselection; self.preselection
self.delete_colliding_marks(self.preselection, self.cur_row, self.cur_col);
} }
} }
} }
@ -111,93 +113,9 @@ 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 => {
@ -210,8 +128,6 @@ 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;
@ -223,12 +139,24 @@ impl State {
self.mode = self.next_mode; self.mode = self.next_mode;
} }
fn get_elapsed_time(&self) -> time::Duration { pub fn move_cursor_to(&mut self, row: usize, col: usize) {
time::Instant::now() - self.start_time 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;
} }
/// returns number of filled cells pub fn toggle_current_mark(&mut self) {
pub fn get_completion_string(&self) -> String { self.markups[self.cur_row][self.cur_col][self.preselection as usize] ^= true;
}
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 {
@ -238,12 +166,9 @@ 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)].iter().collect() [to_char(count / 10), to_char(count % 10)]
} }
/// 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
@ -251,147 +176,40 @@ impl State {
.flatten() .flatten()
.filter(|&cell| cell == self.preselection) .filter(|&cell| cell == self.preselection)
.count(); .count();
match count { (count.min(9) as u8 + b'0') as char
10.. => '!',
_ => (count as u8 + b'0') as char,
}
} }
/// returns the difficulty string used on the ingame scoreboard. pub fn get_difficulty_chars(&self) -> [char; 5] {
/// if you want the complete difficulty names use `Difficulty::to_string()`
pub fn get_difficulty_string(&self) -> String {
match self.difficulty { match self.difficulty {
Difficulty::Easy => String::from("Easy "), Difficulty::Easy => [' ', 'E', 'a', 's', 'y'],
Difficulty::Mid => String::from(" Mid "), Difficulty::Mid => [' ', 'M', 'i', 'd', ' '],
Difficulty::Hard => String::from("Hard "), Difficulty::Hard => [' ', 'H', 'a', 'r', 'd'],
Difficulty::Expert => String::from("Exprt"), Difficulty::Expert => ['E', 'x', 'p', 'r', 't'],
Difficulty::Custom(x) => format!("C({:02})", x), Difficulty::Custom(x) => [
'C',
'(',
(x as u8 / 10 + b'0') as char,
(x as u8 % 10 + b'0') as char,
')',
],
} }
} }
/// returns (mins, secs) as a pair of strings pub fn get_timer_chars(&self) -> [char; 5] {
/// both are zero-padded to a width of 2 characters let mut secs = self.get_time().as_secs();
pub fn get_timer_strings(&self) -> (String, String) { let mut chars = [':'; 5];
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; let to_char = |x: u64| (x as u8 + b'0') as char;
chars[0] = to_char(secs / 600);
mins.push(to_char(n / 600)); secs %= 600;
n %= 600; chars[1] = to_char(secs / 60);
mins.push(to_char(n / 60)); secs %= 60;
n %= 60; chars[3] = to_char(secs / 10);
secs %= 10;
secs.push(to_char(n / 10)); chars[4] = to_char(secs);
n %= 10; chars
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 {
let (mins, secs) = self.get_timer_strings(); self.get_timer_chars().iter().collect()
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)),
}

View File

@ -1,3 +1,6 @@
// 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};
@ -6,7 +9,6 @@ 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 {
@ -23,10 +25,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 => 31, Easy => 35,
Mid => 39, Mid => 45,
Hard => 49, Hard => 52,
Expert => 61, Expert => 62,
Custom(x) => *x, Custom(x) => *x,
} }
} }
@ -45,8 +47,6 @@ 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,15 +65,13 @@ 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();

View File

@ -11,7 +11,6 @@ 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]
} }

View File

@ -1,6 +1,8 @@
// 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
View File

@ -1,95 +1,65 @@
use crate::state::*; use crate::state::*;
use std::cmp::Ordering::*;
use std::io; use std::io;
use crossterm::{ use crossterm::{
cursor::{ cursor::{MoveTo, MoveToColumn, SetCursorStyle},
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,
}, },
}; };
/// state of the user interface pub struct Screen<T>
pub struct Ui<T>
where where
T: io::Write, T: io::Write,
{ {
pub presel_color_pair: (Color, Color), pub data: Vec<Vec<char>>,
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> Ui<T> impl<T> Screen<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 presel_color_pair = (Color::Black, Color::Cyan); let data = RENDER_TEMPLATE.iter().map(|s| s.to_vec()).collect();
let markup_color_background = Color::Cyan;
let mut ui = Ui { let mut screen = Screen {
presel_color_pair, data,
markup_color_background,
ostream, ostream,
width, width,
height, height,
}; };
queue!(ui.ostream, SavePosition, EnterAlternateScreen, Clear(All))
.expect("[-]: Error: ui::init: Failed to enter alternate screen.");
enable_raw_mode().expect("[-]: Error: ui::init: Failed to enable raw mode."); enable_raw_mode().expect("[-]: Error: ui::init: Failed to enable raw mode.");
ui queue!(screen.ostream, EnterAlternateScreen)
.expect("[-]: Error: ui::init: Failed to enter alternate screen.");
screen
} }
/// resets terminal state that `Ui::init()` sets: pub fn deinit(&mut self) {
/// - disables raw mode disable_raw_mode().unwrap_or(());
/// - leaves alternate screen buffer execute!(self.ostream, LeaveAlternateScreen).unwrap_or(());
/// - 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(())
} }
@ -98,171 +68,105 @@ where
queue!(self.ostream, MoveToColumn(0)) queue!(self.ostream, MoveToColumn(0))
} }
/// draws the entire ui, watching for possibly changed screen dimensions. pub fn draw(&mut self, row: usize, col: usize) -> io::Result<()> {
/// all other `draw_...()` functions just `queue!(...)` the actions to draw self.draw_screen()?;
/// their respective ui element. self.draw_cursor(row, col)?;
/// this function chains all of these calls in order and then flushes the self.ostream.flush()?;
/// 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(())
} }
/// `queue!(...)`s the drawing of the numbers in the cells. pub fn draw_screen(&mut self) -> io::Result<()> {
/// self.clear()?;
/// NOTE: this function itself does not flush to `self.ostream` let lft_pad = self.width / 2 - 14;
/// in order to only have to flush once per frame. let bot_pad = self.height / 2 - 7;
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 {
match col { let i = row + 1 + row / 3;
3 | 6 => self.move_cursor_by(4, 0), let j = col + 2 + col + col / 3 * 3;
_ => self.move_cursor_by(1, 0),
}?;
let chr = match state.board[row][col] { self.data[i][j] = match state.board[row][col] {
x if x == state.preselection => { 0 => ' ',
queue!( x => (x + b'0') as char,
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)?; fn render_scoreboard_elements(&mut self, state: &State) {
} // char indeces of scoreboard elements
Ok(()) // -----
//
// difficulty [1][34] - [1][38]
// completion [2][34] - [1][35]
//
// timer [4][34] - [3][38]
//
// > 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.data[1][i] = c;
} }
/// `queue!(...)`s the drawing of variable scoreboard content for (i, c) in (34..36).zip(state.get_completion_chars()) {
/// self.data[2][i] = c;
/// NOTE: this function itself does not flush to `self.ostream`
/// in order to only have to flush once per frame.
fn draw_scoreboard(&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(34, 1)?;
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)?;
} }
self.move_cursor_by(0, -3)?; for (i, c) in (34..39).zip(state.get_timer_chars()) {
self.data[4][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)?;
} }
self.move_cursor_by(2, 1)?; for i in 6..9 {
write!(self.ostream, "{}", (state.preselection + b'0') as char)?; self.data[i][33] = ' ';
self.move_cursor_by(-3, 1)?;
write!(self.ostream, "{}", state.get_preselection_completion_char())?;
queue!(self.ostream, SetForegroundColor(Color::Reset))?;
Ok(())
} }
/// `queue!(...)`s the placement of the cursor on the selected cell. self.data[match state.mode {
/// Mode::Edit => 6,
/// NOTE: this function itself does not flush to `self.ostream` Mode::Markup => 7,
/// in order to only have to flush once per frame. Mode::Go => 8,
fn draw_cursor(&mut self, state: &State) -> io::Result<()> { }][33] = '>';
let (row, col) = (state.cur_row, state.cur_col);
let (x, y) = ((self.width / 2 - 14) as u16, (self.height / 2 - 6) as u16); 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<()> {
let (x, y) = (
(self.width / 2 - 14) as u16,
(self.height / 2 - 7 + (self.height & 1)) 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 {
@ -277,70 +181,72 @@ 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 const RENDER_TEMPLATE: [[char; 41]; 13] = [
/// 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))
}
}
/// 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 │"), '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ',
String::from("└────────┴────────┴────────┘ └───────┘"), '/', '8', '1', ' ', '│',
] ],
} [
'│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '│', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
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', ' ', '│',
],
[
'└', '─', '─', '─', '─', '─', '─', '─', '─', '┴', '─', '─', '─', '─', '─', '─', '─', '─',
'┴', '─', '─', '─', '─', '─', '─', '─', '─', '┘', ' ', ' ', ' ', ' ', '└', '─', '─', '─',
'─', '─', '─', '─', '┘',
],
];