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