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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
//! Manages the state of the `FreeCell` game

use circular_buffer::CircularBuffer;

use rand::seq::SliceRandom;

use crate::cards::{new_standard_deck, Card};

/// The total number of ranks in a standard deck of cards.
const RANKS: u8 = 13;
/// The total number of suits in a standard deck of cards.
const SUITS: u8 = 4;
/// The total number of cards in a standard deck.
const DECK_SIZE: usize = RANKS as usize * SUITS as usize;

/// Constant representing the suit of Hearts.
const HEARTS: u8 = 1;
/// Constant representing the suit of Clubs.
const CLUBS: u8 = 2;
/// Constant representing the suit of Diamonds.
const DIAMONDS: u8 = 3;
/// Constant representing the suit of Spades.
const SPADES: u8 = 4;

/// The number of foundation piles in the game, one for each suit.
const FOUNDATIONS: usize = SUITS as usize;
/// The number of free cells available for storing cards temporarily.
const FREE_CELLS: usize = 4;
/// The number of tableau piles in the game.
const TABLEAU_SIZE: usize = 8;
/// The total size of the game field, including foundations, suits, free cells and tableau piles.
const FIELD_SIZE: usize = FOUNDATIONS + FREE_CELLS + TABLEAU_SIZE;

/// The maximum number of undo levels the game supports.
const UNDO_LEVELS: usize = 1000;

/// Represents a move in the game, indicating the source and destination stack indices on the game field.
#[derive(Default, Copy, Clone)]
struct Move {
    /// The index of the stack the card is moved from.
    from: usize,
    /// The index of the stack the card is moved to.
    to: usize
}

/// Represents the state of a `FreeCell` game.
pub struct Game {
    /// The playing field, consisting of stacks of cards.
    field: [Vec<Card>; FIELD_SIZE],

    /// The index of the card the player currently has highlighted
    highlighted_card: usize,

    /// The index of the card the player has marked to be moved, if any.
    selected_card_opt: Option<usize>,

    /// The circular buffer storing the game's undo history.
    undo_history: CircularBuffer<UNDO_LEVELS, Move>,

    /// The number of moves made so far in the game.
    move_count: u32,

    /// Indicates whether the game is in high contrast mode, where each suit is printed in a different color.
    high_contrast: bool,
}

impl Game {
    /// Creates a new instance of the `FreeCell` game.
    ///
    /// # Arguments
    ///
    /// * `rng` - A mutable reference to a random number generator.
    ///
    /// # Returns
    ///
    /// A new `Game` instance.
    pub fn new(rng: &mut rand::rngs::ThreadRng) -> Game {
        let mut game = Game {
            field: core::array::from_fn(|_| Vec::with_capacity(DECK_SIZE)),
            highlighted_card: FOUNDATIONS + FREE_CELLS,
            selected_card_opt: None,
            undo_history: CircularBuffer::new(),
            move_count: 0,
            high_contrast: false
        };

        // Deal deck onto the board
        let mut deck = new_standard_deck(RANKS, SUITS);
        deck.shuffle(rng);
        //deck.sort_by_key(|card| card.rank); // for testing
        //deck.reverse(); // for testing
        for (i, card) in deck.into_iter().enumerate() {
            let field_column = FOUNDATIONS + FREE_CELLS + (i % TABLEAU_SIZE);
            game.field[field_column].push(card);
        }

        game
    }

    /// Checks if the game has been won.
    ///
    /// # Returns
    ///
    /// `true` if the game has been won, otherwise `false`.
    pub fn is_won(&self) -> bool {
        // Check if all foundation piles are full
        self.field.iter().take(FOUNDATIONS).all(|stack| stack.len() == RANKS as usize)
    }
    
    /// Toggles high contrast mode, making diamonds magenta and spades yellow.
    pub fn toggle_high_contrast(&mut self) {
        self.high_contrast = !self.high_contrast;
    }

    /// Moves the cursor to the left on the game field, skipping invalid spots.
    pub fn move_cursor_left(&mut self) {
        // this modulo trick avoids negative numbers on the unsigned int
        self.highlighted_card = (self.highlighted_card + FIELD_SIZE - 1) % FIELD_SIZE;

        match self.selected_card_opt {
            Some(selected_card) => {
                while !self.move_is_valid(selected_card, self.highlighted_card) && selected_card != self.highlighted_card {
                    self.move_cursor_left();
                }
            }
            None => {
                while self.field[self.highlighted_card].last().is_none() {
                    self.move_cursor_left();
                }
            }
        }
    }

    /// Moves the cursor to the right on the game field, skipping invalid spots.
    pub fn move_cursor_right(&mut self) {
        self.highlighted_card = (self.highlighted_card + 1) % FIELD_SIZE;

        match self.selected_card_opt {
            Some(selected_card) => {
                while !self.move_is_valid(selected_card, self.highlighted_card) && selected_card != self.highlighted_card {
                    self.move_cursor_right();
                }
            }
            None => {
                while self.field[self.highlighted_card].last().is_none() {
                    self.move_cursor_right();
                }
            }
        }
    }

    /// Quick stacks all visible cards to the foundation piles, recursively.
    pub fn quick_stack_to_foundations(&mut self) {
        let mut made_move = false;

        'outer: for source_column in 0..self.field.len() {
            for target_column in 0..FOUNDATIONS {
                if self.move_is_valid(source_column, target_column) {
                    self.player_try_execute_move(source_column, target_column);
                    made_move = true;
                    break 'outer;
                }
            }
        }
        // If we made a move, check the new board state for more opportunities
        if made_move {self.quick_stack_to_foundations()};
    }

    /// Handles the event where a player clicks space/enter on a card.
    pub fn handle_card_press(&mut self) {
        if self.selected_card_opt.is_none() {
            // Select a card
            self.selected_card_opt = Some(self.highlighted_card);
        } else if Some(self.highlighted_card) == self.selected_card_opt {
            // Deselect a card
            self.selected_card_opt = None;
        } else {
            // Execute a move
            if let Some(selected_card) = self.selected_card_opt {
                self.player_try_execute_move(selected_card, self.highlighted_card);
            }
        }
    }

    /// Executes a player move if it is valid.
    pub fn player_try_execute_move(&mut self, from: usize, to: usize) {
        if self.move_is_valid(from, to) {
            // Execute move, add to undo history
            self.execute_move(from, to);
            self.move_count += 1;
            self.undo_history.push_back(Move{from, to});
        }
    }

    /// Undoes the last move made by the player.
    /// Can be used multiple times to travel back in the game's history.
    pub fn perform_undo(&mut self) {
        let last_move_opt = self.undo_history.pop_back();
        if let Some(last_move) = last_move_opt {
            self.execute_move(last_move.to, last_move.from);
            self.move_count -= 1;
        } // Else history is empty
    }

    /// Checks if two cards are of opposite colors.
    fn are_opposite_colors(card1: Card, card2: Card) -> bool {
        if card1.suit == HEARTS || card1.suit == DIAMONDS {return card2.suit == SPADES || card2.suit == CLUBS};
        if card1.suit == SPADES || card1.suit == CLUBS {return card2.suit == HEARTS || card2.suit == DIAMONDS};
        false
    }

    /// Checks if a move from one position to another is valid.
    fn move_is_valid(&self, from: usize, to: usize) -> bool {
        if from == to {return false;};
        let from_top_card = self.field[from].last().copied().unwrap_or_default();
        let to_top_card = self.field[to].last().copied().unwrap_or_default();
        if to < FOUNDATIONS {
            // Foundation case
            if to_top_card.rank != 0 {
                    return from_top_card.rank == to_top_card.rank + 1 && from_top_card.suit == to_top_card.suit;
            }
            return from_top_card.rank == 1 && to == (from_top_card.suit - 1) as usize;
        } else if to < FOUNDATIONS + FREE_CELLS {
            // Free cell case
            return to_top_card.rank == 0;
        } else if to < FOUNDATIONS + FREE_CELLS + TABLEAU_SIZE {
            // Tableau case
            if to_top_card.rank != 0 {
                return from_top_card.rank == to_top_card.rank - 1 && Game::are_opposite_colors(from_top_card, to_top_card);
            }
            return true;
        }
        false
    }

    /// Executes a move from one position to another, not checking if it follows the rules.
    /// To try executing a move in a way that fails if the move does not follow the rules, use `player_try_execute_move`.
    fn execute_move (&mut self, from: usize, to: usize) {
        // Execute the move
        // Move "from" card to "to" column
        let from_card_opt = self.field[from].last();
        if let Some(&from_card) = from_card_opt {
            self.field[from].pop();
            self.field[to].push(from_card);
        }
        self.selected_card_opt = None;
    }
}

mod print;