numberplace-rs

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

main.rs (6917B)


      1 mod board;
      2 
      3 use board::{Board, GRID_SIZE};
      4 use crossterm::{
      5     event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
      6     execute,
      7     terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
      8 };
      9 use ratatui::{
     10     Frame, Terminal,
     11     backend::{Backend, CrosstermBackend},
     12     layout::{Constraint, Direction, Layout, Rect},
     13     style::{Color, Modifier, Style},
     14     text::{Line, Span},
     15     widgets::{Block, Borders, Paragraph},
     16 };
     17 use std::io;
     18 
     19 struct App {
     20     board: Board,
     21     cursor: (usize, usize), // (row, col)
     22 }
     23 
     24 impl App {
     25     fn new() -> Self {
     26         let mut board = Board::new();
     27         board.generate();
     28         Self {
     29             board,
     30             cursor: (0, 0),
     31         }
     32     }
     33 
     34     fn move_cursor(&mut self, row_delta: isize, col_delta: isize) {
     35         if self.board.is_solved() {
     36             return;
     37         }
     38         let new_row =
     39             (self.cursor.0 as isize + row_delta).clamp(0, (GRID_SIZE - 1) as isize) as usize;
     40         let new_col =
     41             (self.cursor.1 as isize + col_delta).clamp(0, (GRID_SIZE - 1) as isize) as usize;
     42         self.cursor = (new_row, new_col);
     43     }
     44 
     45     fn set_cell_value(&mut self, val: Option<u8>) {
     46         if self.board.is_solved() {
     47             return;
     48         }
     49         self.board.set_value(self.cursor.0, self.cursor.1, val);
     50     }
     51 }
     52 
     53 fn main() -> Result<(), Box<dyn std::error::Error>> {
     54     enable_raw_mode()?;
     55     let mut stdout = io::stdout();
     56     execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
     57     let backend = CrosstermBackend::new(stdout);
     58     let mut terminal = Terminal::new(backend)?;
     59 
     60     let mut app = App::new();
     61     let res = run_app(&mut terminal, &mut app);
     62 
     63     disable_raw_mode()?;
     64     execute!(
     65         terminal.backend_mut(),
     66         LeaveAlternateScreen,
     67         DisableMouseCapture
     68     )?;
     69     terminal.show_cursor()?;
     70 
     71     if let Err(err) = res {
     72         eprintln!("{:?}", err)
     73     }
     74 
     75     Ok(())
     76 }
     77 
     78 fn run_app<B: Backend>(
     79     terminal: &mut Terminal<B>,
     80     app: &mut App,
     81 ) -> Result<(), Box<dyn std::error::Error>>
     82 where
     83     B::Error: std::error::Error + 'static,
     84 {
     85     loop {
     86         terminal.draw(|f| ui(f, app))?;
     87 
     88         if let Event::Key(key) = event::read()? {
     89             match key.code {
     90                 KeyCode::Char('q') => return Ok(()),
     91                 KeyCode::Char('n') if app.board.is_solved() => {
     92                     app.board.generate();
     93                     app.cursor = (0, 0);
     94                 }
     95                 KeyCode::Char('s') => {
     96                     app.board.solve();
     97                 }
     98                 KeyCode::Char('h') | KeyCode::Left => app.move_cursor(0, -1),
     99                 KeyCode::Char('j') | KeyCode::Down => app.move_cursor(1, 0),
    100                 KeyCode::Char('k') | KeyCode::Up => app.move_cursor(-1, 0),
    101                 KeyCode::Char('l') | KeyCode::Right => app.move_cursor(0, 1),
    102                 KeyCode::Char(c) if c.is_digit(10) => {
    103                     let val = c.to_digit(10).unwrap() as u8;
    104                     if val == 0 {
    105                         app.set_cell_value(None);
    106                     } else {
    107                         app.set_cell_value(Some(val));
    108                     }
    109                 }
    110                 KeyCode::Char('x') | KeyCode::Backspace => app.set_cell_value(None),
    111                 _ => {}
    112             }
    113         }
    114     }
    115 }
    116 
    117 fn ui(f: &mut Frame, app: &App) {
    118     let chunks = Layout::default()
    119         .direction(Direction::Vertical)
    120         .constraints([Constraint::Min(13), Constraint::Length(3)].as_ref())
    121         .split(f.area());
    122 
    123     draw_board(f, chunks[0], app);
    124     draw_help(f, chunks[1], app.board.is_solved());
    125 }
    126 
    127 fn draw_board(f: &mut Frame, area: Rect, app: &App) {
    128     let board_width = 9 * 2 + 2;
    129     let board_height = 9 + 2;
    130 
    131     let board_area = Rect::new(
    132         area.x + (area.width.saturating_sub(board_width as u16)) / 2,
    133         area.y + (area.height.saturating_sub(board_height as u16)) / 2,
    134         board_width as u16,
    135         board_height as u16,
    136     );
    137 
    138     let block = Block::default().borders(Borders::ALL).title(" Sudoku ");
    139     f.render_widget(block, board_area);
    140 
    141     let inner = board_area.inner(ratatui::layout::Margin {
    142         vertical: 1,
    143         horizontal: 1,
    144     });
    145 
    146     for r in 0..GRID_SIZE {
    147         for c in 0..GRID_SIZE {
    148             let cell = app.board.cells[r][c];
    149             let is_cursor = (r, c) == app.cursor;
    150             let is_conflict = app.board.is_conflict(r, c);
    151 
    152             let mut style = Style::default();
    153             if cell.is_fixed {
    154                 style = style.add_modifier(Modifier::BOLD);
    155             } else {
    156                 style = style.fg(Color::Cyan);
    157             }
    158 
    159             if is_conflict {
    160                 style = style.bg(Color::Red).fg(Color::White);
    161             }
    162 
    163             if is_cursor {
    164                 style = style.bg(Color::White).fg(Color::Black);
    165             }
    166 
    167             let text = match cell.value {
    168                 Some(v) => v.to_string(),
    169                 None => " ".to_string(),
    170             };
    171 
    172             let x_offset = c as u16 * 2;
    173 
    174             f.render_widget(
    175                 Paragraph::new(text).style(style),
    176                 Rect::new(inner.x + x_offset, inner.y + r as u16, 1, 1),
    177             );
    178 
    179             if c % 3 == 2 && c < 8 {
    180                 f.render_widget(
    181                     Paragraph::new("│").style(Style::default().fg(Color::DarkGray)),
    182                     Rect::new(inner.x + x_offset + 1, inner.y + r as u16, 1, 1),
    183                 );
    184             }
    185         }
    186     }
    187 }
    188 
    189 fn draw_help(f: &mut Frame, area: Rect, solved: bool) {
    190     let help_text = if solved {
    191         vec![Line::from(vec![
    192             Span::styled(
    193                 "CONGRATULATIONS! ",
    194                 Style::default()
    195                     .fg(Color::Yellow)
    196                     .add_modifier(Modifier::BOLD),
    197             ),
    198             Span::raw("Press "),
    199             Span::styled("n", Style::default().add_modifier(Modifier::BOLD)),
    200             Span::raw(" for new game, "),
    201             Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
    202             Span::raw(": quit"),
    203         ])]
    204     } else {
    205         vec![Line::from(vec![
    206             Span::styled("hjkl/Arrows", Style::default().add_modifier(Modifier::BOLD)),
    207             Span::raw(": move, "),
    208             Span::styled("1-9", Style::default().add_modifier(Modifier::BOLD)),
    209             Span::raw(": input, "),
    210             Span::styled("x/0/BS", Style::default().add_modifier(Modifier::BOLD)),
    211             Span::raw(": delete, "),
    212             Span::styled("s", Style::default().add_modifier(Modifier::BOLD)),
    213             Span::raw(": solve, "),
    214             Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
    215             Span::raw(": quit"),
    216         ])]
    217     };
    218     let help =
    219         Paragraph::new(help_text).block(Block::default().borders(Borders::ALL).title(" Help "));
    220     f.render_widget(help, area);
    221 }