rusty_freecell/
game.rs

1//! Manages the state of the `FreeCell` game
2
3use circular_buffer::CircularBuffer;
4
5use rand::seq::SliceRandom;
6
7use crate::cards::{new_standard_deck, Card};
8
9/// The total number of ranks in a standard deck of cards.
10const RANKS: u8 = 13;
11/// The total number of suits in a standard deck of cards.
12const SUITS: u8 = 4;
13/// The total number of cards in a standard deck.
14const DECK_SIZE: usize = RANKS as usize * SUITS as usize;
15
16/// Constant representing the suit of Hearts.
17const HEARTS: u8 = 1;
18/// Constant representing the suit of Clubs.
19const CLUBS: u8 = 2;
20/// Constant representing the suit of Diamonds.
21const DIAMONDS: u8 = 3;
22/// Constant representing the suit of Spades.
23const SPADES: u8 = 4;
24
25/// The number of foundation piles in the game, one for each suit.
26const FOUNDATIONS: usize = SUITS as usize;
27/// The number of free cells available for storing cards temporarily.
28const FREE_CELLS: usize = 4;
29/// The number of tableau piles in the game.
30const TABLEAU_SIZE: usize = 8;
31/// The total size of the game field, including foundations, suits, free cells and tableau piles.
32const FIELD_SIZE: usize = FOUNDATIONS + FREE_CELLS + TABLEAU_SIZE;
33
34/// The maximum number of undo levels the game supports.
35const UNDO_LEVELS: usize = 1000;
36
37/// Represents a move in the game, indicating the source and destination stack indices on the game field.
38#[derive(Default, Copy, Clone)]
39struct Move {
40    /// The index of the stack the card is moved from.
41    from: usize,
42    /// The index of the stack the card is moved to.
43    to: usize
44}
45
46/// Represents the state of a `FreeCell` game.
47pub struct Game {
48    /// The playing field, consisting of stacks of cards.
49    field: [Vec<Card>; FIELD_SIZE],
50
51    /// The index of the card the player currently has highlighted
52    highlighted_card: usize,
53
54    /// The index of the card the player has marked to be moved, if any.
55    selected_card_opt: Option<usize>,
56
57    /// The circular buffer storing the game's undo history.
58    undo_history: CircularBuffer<UNDO_LEVELS, Move>,
59
60    /// The number of moves made so far in the game.
61    move_count: u32,
62
63    /// Indicates whether the game is in high contrast mode, where each suit is printed in a different color.
64    high_contrast: bool,
65}
66
67impl Game {
68    /// Creates a new instance of the `FreeCell` game.
69    ///
70    /// # Arguments
71    ///
72    /// * `rng` - A mutable reference to a random number generator.
73    ///
74    /// # Returns
75    ///
76    /// A new `Game` instance.
77    pub fn new(rng: &mut rand::rngs::ThreadRng) -> Game {
78        let mut game = Game {
79            field: core::array::from_fn(|_| Vec::with_capacity(DECK_SIZE)),
80            highlighted_card: FOUNDATIONS + FREE_CELLS,
81            selected_card_opt: None,
82            undo_history: CircularBuffer::new(),
83            move_count: 0,
84            high_contrast: false
85        };
86
87        // Deal deck onto the board
88        let mut deck = new_standard_deck(RANKS, SUITS);
89        deck.shuffle(rng);
90        //deck.sort_by_key(|card| card.rank); // for testing
91        //deck.reverse(); // for testing
92        for (i, card) in deck.into_iter().enumerate() {
93            let field_column = FOUNDATIONS + FREE_CELLS + (i % TABLEAU_SIZE);
94            game.field[field_column].push(card);
95        }
96
97        game
98    }
99
100    /// Checks if the game has been won.
101    ///
102    /// # Returns
103    ///
104    /// `true` if the game has been won, otherwise `false`.
105    pub fn is_won(&self) -> bool {
106        // Check if all foundation piles are full
107        self.field.iter().take(FOUNDATIONS).all(|stack| stack.len() == RANKS as usize)
108    }
109    
110    /// Toggles high contrast mode, making diamonds magenta and spades yellow.
111    pub fn toggle_high_contrast(&mut self) {
112        self.high_contrast = !self.high_contrast;
113    }
114
115    /// Moves the cursor to the left on the game field, skipping invalid spots.
116    pub fn move_cursor_left(&mut self) {
117        // this modulo trick avoids negative numbers on the unsigned int
118        self.highlighted_card = (self.highlighted_card + FIELD_SIZE - 1) % FIELD_SIZE;
119
120        match self.selected_card_opt {
121            Some(selected_card) => {
122                while !self.move_is_valid(selected_card, self.highlighted_card) && selected_card != self.highlighted_card {
123                    self.move_cursor_left();
124                }
125            }
126            None => {
127                while self.field[self.highlighted_card].last().is_none() {
128                    self.move_cursor_left();
129                }
130            }
131        }
132    }
133
134    /// Moves the cursor to the right on the game field, skipping invalid spots.
135    pub fn move_cursor_right(&mut self) {
136        self.highlighted_card = (self.highlighted_card + 1) % FIELD_SIZE;
137
138        match self.selected_card_opt {
139            Some(selected_card) => {
140                while !self.move_is_valid(selected_card, self.highlighted_card) && selected_card != self.highlighted_card {
141                    self.move_cursor_right();
142                }
143            }
144            None => {
145                while self.field[self.highlighted_card].last().is_none() {
146                    self.move_cursor_right();
147                }
148            }
149        }
150    }
151
152    /// Quick stacks all visible cards to the foundation piles, recursively.
153    pub fn quick_stack_to_foundations(&mut self) {
154        let mut made_move = false;
155
156        'outer: for source_column in 0..self.field.len() {
157            for target_column in 0..FOUNDATIONS {
158                if self.move_is_valid(source_column, target_column) {
159                    self.player_try_execute_move(source_column, target_column);
160                    made_move = true;
161                    break 'outer;
162                }
163            }
164        }
165        // If we made a move, check the new board state for more opportunities
166        if made_move {self.quick_stack_to_foundations()};
167    }
168
169    /// Handles the event where a player clicks space/enter on a card.
170    pub fn handle_card_press(&mut self) {
171        if self.selected_card_opt.is_none() {
172            // Select a card
173            self.selected_card_opt = Some(self.highlighted_card);
174        } else if Some(self.highlighted_card) == self.selected_card_opt {
175            // Deselect a card
176            self.selected_card_opt = None;
177        } else {
178            // Execute a move
179            if let Some(selected_card) = self.selected_card_opt {
180                self.player_try_execute_move(selected_card, self.highlighted_card);
181            }
182        }
183    }
184
185    /// Executes a player move if it is valid.
186    pub fn player_try_execute_move(&mut self, from: usize, to: usize) {
187        if self.move_is_valid(from, to) {
188            // Execute move, add to undo history
189            self.execute_move(from, to);
190            self.move_count += 1;
191            self.undo_history.push_back(Move{from, to});
192        }
193    }
194
195    /// Undoes the last move made by the player.
196    /// Can be used multiple times to travel back in the game's history.
197    pub fn perform_undo(&mut self) {
198        let last_move_opt = self.undo_history.pop_back();
199        if let Some(last_move) = last_move_opt {
200            self.execute_move(last_move.to, last_move.from);
201            self.move_count -= 1;
202        } // Else history is empty
203    }
204
205    /// Checks if two cards are of opposite colors.
206    fn are_opposite_colors(card1: Card, card2: Card) -> bool {
207        if card1.suit == HEARTS || card1.suit == DIAMONDS {return card2.suit == SPADES || card2.suit == CLUBS};
208        if card1.suit == SPADES || card1.suit == CLUBS {return card2.suit == HEARTS || card2.suit == DIAMONDS};
209        false
210    }
211
212    /// Checks if a move from one position to another is valid.
213    fn move_is_valid(&self, from: usize, to: usize) -> bool {
214        if from == to {return false;};
215        let from_top_card = self.field[from].last().copied().unwrap_or_default();
216        let to_top_card = self.field[to].last().copied().unwrap_or_default();
217        if to < FOUNDATIONS {
218            // Foundation case
219            if to_top_card.rank != 0 {
220                    return from_top_card.rank == to_top_card.rank + 1 && from_top_card.suit == to_top_card.suit;
221            }
222            return from_top_card.rank == 1 && to == (from_top_card.suit - 1) as usize;
223        } else if to < FOUNDATIONS + FREE_CELLS {
224            // Free cell case
225            return to_top_card.rank == 0;
226        } else if to < FOUNDATIONS + FREE_CELLS + TABLEAU_SIZE {
227            // Tableau case
228            if to_top_card.rank != 0 {
229                return from_top_card.rank == to_top_card.rank - 1 && Game::are_opposite_colors(from_top_card, to_top_card);
230            }
231            return true;
232        }
233        false
234    }
235
236    /// Executes a move from one position to another, not checking if it follows the rules.
237    /// To try executing a move in a way that fails if the move does not follow the rules, use `player_try_execute_move`.
238    fn execute_move (&mut self, from: usize, to: usize) {
239        // Execute the move
240        // Move "from" card to "to" column
241        let from_card_opt = self.field[from].last();
242        if let Some(&from_card) = from_card_opt {
243            self.field[from].pop();
244            self.field[to].push(from_card);
245        }
246        self.selected_card_opt = None;
247    }
248}
249
250mod print;