1use 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
11const TYPICAL_BOARD_HEIGHT: u16 = 24;
13
14const CARD_PRINT_WIDTH: u16 = 7;
16const CARD_PRINT_HEIGHT: u16 = 5;
18const TABLEAU_VERTICAL_OFFSET: u16 = 2;
20
21const DEFAULT_TERMINAL_WIDTH: u16 = 80;
23const DEFAULT_TERMINAL_HEIGHT: u16 = 24;
25
26const SUIT_STRINGS: [&str; SUITS as usize + 1] = [" ", "♥", "♣", "♦", "♠"];
28const RANK_STRINGS: [&str; RANKS as usize + 1] = [" ", "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];
30
31impl Game {
32 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 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 #[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 #[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 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(); #[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 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 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 out.queue(cursor::MoveTo(0, 0))?;
133 print!("╭── Rusty FreeCell ────────────────────────────────────────╮");
134 out.queue(cursor::MoveTo(40, 0))?;
135 print!(" Moves: {move_count} ");
136
137 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 out.queue(cursor::MoveTo(0, term_height))?;
148 print!("╰── (New Game: ctrl-n) ─ (Undo: z) ─ (Quit: ctrl-q) ───────╯");
149
150 Ok(())
151 }
152
153 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 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 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 let _= out.queue(style::SetAttribute(style::Attribute::NormalIntensity));
229 }
230 }
231 Ok(())
232 }
233
234 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 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