rusty_freecell/game/
print.rs

1//! Utilities for printing the state of the `FreeCell` game to the terminal
2
3use std::io::{self, Write};
4
5use crossterm::{cursor, style::{self, Stylize}, terminal, QueueableCommand};
6
7use crate::{cards::Card, game::Game, MIN_TERMINAL_WIDTH};
8
9use super::{CLUBS, DIAMONDS, FREE_CELLS, HEARTS, RANKS, SPADES, SUITS, FOUNDATIONS, TABLEAU_SIZE};
10
11/// Typical height of the game board in number of lines. This is used for vertically centering the win screen.
12const TYPICAL_BOARD_HEIGHT: u16 = 24;
13
14/// Width of a printed card in characters.
15const CARD_PRINT_WIDTH: u16 = 7;
16/// Width of a printed card in characters.
17const CARD_PRINT_HEIGHT: u16 = 5;
18/// Vertical printing offset (measured in characters) between cards that are stacked on top of each other on the tableau.
19const TABLEAU_VERTICAL_OFFSET: u16 = 2;
20
21/// Default width of the terminal window.
22const DEFAULT_TERMINAL_WIDTH: u16 = 80;
23/// Default height of the terminal window.
24const DEFAULT_TERMINAL_HEIGHT: u16 = 24;
25
26/// Strings representing suits in the order: empty, hearts, clubs, diamonds, spades.
27const SUIT_STRINGS: [&str; SUITS as usize + 1] = [" ", "♥", "♣", "♦", "♠"];
28/// Strings representing ranks in the order: empty, A, 2, 3, ..., 10, J, Q, K.
29const RANK_STRINGS: [&str; RANKS as usize + 1] = [" ", "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];
30
31impl Game {
32    /// Prints the game state to the terminal.
33    ///
34    /// # Arguments
35    ///
36    /// * `out` - A mutable reference to the standard output stream.
37    ///
38    /// # Returns
39    ///
40    /// A Result indicating success or an error if writing to the output stream fails.
41    pub fn print(&self, out: &mut io::Stdout) -> Result<(), io::Error> {
42        if self.is_won() {
43            out.queue(style::SetAttribute(style::Attribute::Dim))?;
44            self.print_board(out)?;
45            out.queue(style::SetAttribute(style::Attribute::Reset))?;
46            Game::print_chrome(out, self.move_count)?;
47            Game::print_win(out)?;
48        } else {
49            self.print_board(out)?;
50            Game::print_chrome(out, self.move_count)?;
51        }
52        out.flush()?;
53        Ok(())
54    }
55
56    /// Prints the game board layout to the terminal.
57    fn print_board(&self, out: &mut io::Stdout) -> Result<(), io::Error> {
58        out.queue(terminal::Clear(terminal::ClearType::All))?;
59
60        for (i, stack) in self.field.iter().enumerate() {
61            let mut top_card = stack.last().copied().unwrap_or_default();
62            let top_card_is_highlighted = self.highlighted_card == i && !self.is_won();
63            if i < FOUNDATIONS {
64                // Print foundation
65                // If card is a placeholder, assign a suit for decoration
66                #[allow(clippy::cast_possible_truncation)]
67                if top_card == Card::default() {
68                    top_card = Card{rank: 0, suit: i as u8 + 1};
69                }
70                #[allow(clippy::cast_possible_truncation)]
71                Game::print_card_at_coord(
72                    out,
73                    i as u16 * CARD_PRINT_WIDTH + 1, 
74                    1, 
75                    top_card, 
76                    top_card_is_highlighted, 
77                    self.selected_card_opt == Some(i),
78                    self.high_contrast
79                )?;
80            } else if i < FOUNDATIONS + FREE_CELLS {
81                // Print free cells
82                #[allow(clippy::cast_possible_truncation)]
83                Game::print_card_at_coord(
84                    out,
85                    i as u16 * CARD_PRINT_WIDTH + 3,
86                    1,
87                    top_card,
88                    top_card_is_highlighted,
89                    self.selected_card_opt == Some(i),
90                    self.high_contrast
91                )?;
92            } else if i < FOUNDATIONS + FREE_CELLS + TABLEAU_SIZE {
93                // Print tableau column card-by-card
94                let mut card_stack_iter = stack.iter().enumerate().peekable();
95                while let Some((y, &card)) = card_stack_iter.next() {
96                    let is_top_card = card_stack_iter.peek().is_none(); // Check if we are currently printing the top card
97                    #[allow(clippy::cast_possible_truncation)]
98                    Game::print_card_at_coord(
99                        out,
100                        (i as u16 - (FOUNDATIONS as u16 + FREE_CELLS as u16)) * CARD_PRINT_WIDTH + 2,
101                        y as u16 * TABLEAU_VERTICAL_OFFSET + CARD_PRINT_HEIGHT + 1,
102                        card,
103                        top_card_is_highlighted && is_top_card,
104                        self.selected_card_opt == Some(i) && is_top_card,
105                        self.high_contrast,
106                    )?;
107                }
108                // If tableau column is empty, print placeholder instead
109                if stack.is_empty() {
110                    #[allow(clippy::cast_possible_truncation)]
111                    Game::print_card_at_coord(
112                        out,
113                        (i as u16 - (FOUNDATIONS as u16 + FREE_CELLS as u16)) * CARD_PRINT_WIDTH + 2,
114                        CARD_PRINT_HEIGHT + 1,
115                        top_card,
116                        top_card_is_highlighted,
117                        self.selected_card_opt == Some(i),
118                        self.high_contrast
119                    )?;
120                }
121            }
122        }
123
124        Ok(())
125    }
126
127    /// Prints the game chrome (title, side bars, etc.) to the terminal.
128    fn print_chrome(out: &mut std::io::Stdout, move_count: u32) -> Result<(), io::Error> {
129        let (_term_width, term_height) = terminal::size().unwrap_or((DEFAULT_TERMINAL_WIDTH, DEFAULT_TERMINAL_HEIGHT));
130        
131        // Print title bar
132        out.queue(cursor::MoveTo(0, 0))?;
133        print!("╭── Rusty FreeCell ────────────────────────────────────────╮");
134        out.queue(cursor::MoveTo(40, 0))?;
135        print!(" Moves: {move_count} ");
136
137        // Print side bars
138
139        for i in 1..term_height {
140            out.queue(cursor::MoveTo(0, i))?;
141            print!("│");
142            out.queue(cursor::MoveTo(crate::MIN_TERMINAL_WIDTH - 1, i))?;
143            print!("│");
144        }
145
146        // Print bottom bar
147        out.queue(cursor::MoveTo(0, term_height))?;
148        print!("╰── (New Game: ctrl-n) ─ (Undo: z) ─ (Quit: ctrl-q) ───────╯");
149
150        Ok(())
151    }
152
153    /// Prints a card at the specified coordinates on the terminal.
154    fn print_card_at_coord(out: &mut io::Stdout, x: u16, y: u16, card: Card, highlighted: bool, selected: bool, high_contrast: bool)  -> Result<(), io::Error> {
155        let card_suit_rank_str = RANK_STRINGS[card.rank as usize].to_owned() + SUIT_STRINGS[card.suit as usize];
156        let card_display_str;
157        if selected {
158            card_display_str= format!("\
159                ╭─────╮\n\
160                │ {card_suit_rank_str: <3} │\n\
161                │     │\n\
162                │  △  │\n\
163                ╰─────╯\n");
164        } else if card.rank == 0 {
165            // Print suit-decorated placeholder
166            card_display_str= format!("\
167            ╭─────╮\n\
168            │     │\n\
169            │ {card_suit_rank_str}  │\n\
170            │     │\n\
171            ╰─────╯\n");
172        } else {
173            card_display_str= format!("\
174            ╭─────╮\n\
175            │ {card_suit_rank_str: <3} │\n\
176            │     │\n\
177            │     │\n\
178            ╰─────╯\n");
179        }
180
181        for (d, line) in card_display_str.lines().enumerate() {
182            #[allow(clippy::cast_possible_truncation)]
183            out.queue(cursor::MoveTo(x, y + d as u16))?;
184            if highlighted {
185                let _= out.queue(style::SetAttribute(style::Attribute::Reverse));
186            } else if card.rank == 0 {
187                // dim placeholder
188                let _= out.queue(style::SetAttribute(style::Attribute::Dim));
189            }
190
191            if card.rank != 0 {
192                if high_contrast {
193                    match card.suit {
194                        HEARTS => {
195                            print!("{}", line.with(style::Color::DarkRed));
196                        },
197                        CLUBS => {
198                            print!("{}", line.with(style::Color::White));
199                        },
200                        DIAMONDS => {
201                            print!("{}", line.with(style::Color::Magenta));
202                        },
203                        SPADES => {
204                            print!("{}", line.with(style::Color::Yellow));
205                        },
206                        _ => {
207                            print!("{line}");
208                        }
209                    }
210                } else {
211                    match card.suit {
212                        HEARTS | DIAMONDS  => {
213                            print!("{}", line.with(style::Color::Red));
214                        },
215                        _ => {
216                            print!("{line}");
217                        }
218                    }
219                }
220            } else {
221                print!("{line}");
222            }
223
224            if highlighted {
225                let _= out.queue(style::SetAttribute(style::Attribute::NoReverse));
226            } else if card.rank == 0 {
227                // undim placeholder
228                let _= out.queue(style::SetAttribute(style::Attribute::NormalIntensity));
229            }
230        }
231        Ok(())
232    }
233
234    /// Prints the win message to the terminal.
235    fn print_win (out: &mut io::Stdout) -> Result<(), io::Error> {
236        let win_message_width = 20;
237        let win_message_height = 4;
238        Game::print_string_at_coord(out,   
239        "╭──────────────────╮\n\
240                 │ You Win!         │\n\
241                 │ New Game: ctrl-n │\n\
242                 ╰──────────────────╯",
243                MIN_TERMINAL_WIDTH / 2 - win_message_width / 2,
244                TYPICAL_BOARD_HEIGHT / 2 - win_message_height / 2)?;
245        Ok(())
246    }
247
248    /// Prints a string at the specified coordinates on the terminal.
249    fn print_string_at_coord(out: &mut io::Stdout, string: &str, x: u16, y: u16) -> Result<(), io::Error> {
250        for (i, line) in string.lines().enumerate() {
251            #[allow(clippy::cast_possible_truncation)]
252            out.queue(cursor::MoveTo(x, y + i as u16))?;
253            print!("{line}");
254        }
255        Ok(())
256    }
257}
258