rusty_freecell/
main.rs

1//! A `FreeCell` game written in Rust
2
3#![warn(
4    missing_docs,
5    clippy::all,
6    clippy::pedantic,
7    clippy::missing_docs_in_private_items
8)]
9
10mod game;
11mod cards;
12
13use crate::game::Game;
14
15use std::io::{self, stdout};
16
17use crossterm::{
18    cursor, terminal, ExecutableCommand
19};
20
21/// Minimum width of the terminal window supported by the game.
22const MIN_TERMINAL_WIDTH: u16 = 60;
23/// Minimum height of the terminal window supported by the game.
24const MIN_TERMINAL_HEIGHT: u16 = 24;
25
26/// Runs the game loop.
27///
28/// # Errors
29///
30/// Returns an `io::Error` if there is an issue with terminal I/O.
31fn run() -> Result<(), io::Error> {
32    // Prepare terminal
33    terminal::enable_raw_mode()?;
34    let mut stdout = stdout();
35    stdout.execute(terminal::EnterAlternateScreen)?;
36    stdout.execute(cursor::Hide)?;
37    stdout.execute(terminal::Clear(terminal::ClearType::All))?;
38
39    // Create game
40    let mut rng = rand::thread_rng();
41    let mut game = Game::new(&mut rng);
42    game.print(&mut stdout)?;
43
44    // Game loop
45    loop {
46        let event = crossterm::event::read()?;
47        match event {
48            crossterm::event::Event::Key(key_event) => {
49                use crossterm::event::{KeyModifiers as MOD, KeyCode::{Char, Left, Right, Enter}, KeyEventKind::{Press, Repeat}};
50                if key_event.kind == Press || key_event.kind == Repeat {
51                    match (key_event.code, key_event.modifiers) {
52                        (Left | Char('a'), MOD::NONE) => {
53                            if !game.is_won() {game.move_cursor_left();}
54                        },
55                        (Right | Char('d') , MOD::NONE) => {
56                            if !game.is_won() {game.move_cursor_right();}
57                        },
58                        (Char(' ') | Enter, MOD::NONE) => {
59                            if !game.is_won() {game.handle_card_press();}
60                        },
61                        (Char('z'), MOD::NONE) => {
62                            game.perform_undo();
63                        },
64                        (Char('h'), MOD::NONE) => {
65                            game.toggle_high_contrast();
66                        },
67                        (Char('f'), MOD::NONE) => {
68                            game.quick_stack_to_foundations();
69                        },
70                        (Char('n'), MOD::CONTROL) => {
71                            game = Game::new(&mut rng);
72                        },
73                        (Char('q'), MOD::CONTROL) => {
74                            break
75                        },
76                        _ => {
77                            
78                        }
79                    }
80                }
81            },
82            crossterm::event::Event::Resize(_term_width, _term_height) => {
83                // Resize event falls through and triggers game to print again
84            }
85            _ => {}
86        }
87        game.print(&mut stdout)?;
88    }
89    Ok(())
90}
91
92/// Cleans up the terminal after the game finishes or is interrupted.
93/// This function restores the terminal to its normal state, showing the cursor and disabling raw mode.
94fn cleanup() {
95    let mut stdout = stdout();
96    // Do not catch errors here. By the time we cleanup, we want to execute as many of these as possible to reset the terminal.
97    let _ = stdout.execute(cursor::Show);
98    let _ = terminal::disable_raw_mode();
99    let _ = stdout.execute(terminal::Clear(terminal::ClearType::All));
100    let _ = stdout.execute(terminal::LeaveAlternateScreen);
101    println!();
102}
103
104/// The main function of the `FreeCell` game.
105///
106/// # Errors
107///
108/// Returns an `Err` if the terminal window is too small to play the game.
109fn main() -> Result<(), Box<dyn std::error::Error>> {
110    //std::env::set_var("RUST_BACKTRACE", "1");
111    let (term_width, term_height) = terminal::size()?;
112    if term_width < MIN_TERMINAL_WIDTH || term_height < MIN_TERMINAL_HEIGHT {
113        println!("Your terminal window is too small for FreeCell! It's gotta be at least {MIN_TERMINAL_WIDTH} chars wide and {MIN_TERMINAL_HEIGHT} chars tall.");
114        return Err("terminal too small".into());
115    }
116    run()?;
117    cleanup();
118    Ok(())
119}