cliard24 0.1.2

cliard24 is a command-line 24-point card game. It provides two main functions: the game mode allows you to play the classic 24-point game interactively in the terminal, where you randomly draw 4 cards and use addition, subtraction, multiplication, division, and parentheses to reach 24; the solve mode lets you input any combination of numbers and a target value to calculate all possible expression solutions. The tool supports customizable attempt limits, endless mode, solution count limits, and mixed input of card letters (A/J/Q/K) and numbers.
Documentation
//! # Playing Cards Module
//!
//! This module defines the data structures and functionality for representing and displaying
//! standard playing cards used in the 24-point card game. It supports card creation, indexing,
//! and ASCII art visualization.
//!
//! The module provides:
//! - Card suit and rank representations
//! - Index-based card creation for deck management
//! - ASCII art rendering for terminal display
//! - Type-safe card operations

/// Type alias for card indices in a standard deck (0-51)
///
/// This type is used to uniquely identify each card in a 52-card deck.
/// The index encoding follows the pattern: rank * 4 + suit_index.
pub type CardIdx = u8;

/// Total number of cards in a standard deck without jokers
pub const DECK_SIZE: usize = 52;

/// Represents the four suits in a standard deck of playing cards
///
/// Each suit corresponds to one of the traditional French playing card suits.
/// The ordering (Spades, Hearts, Clubs, Diamonds) follows common convention.
#[cfg_attr(test, derive(Debug, Hash, Eq, PartialEq))]
pub enum CardSuit {
    /// Spades suit - typically black in color
    Spades,
    /// Hearts suit - typically red in color  
    Hearts,
    /// Clubs suit - typically black in color
    Clubs,
    /// Diamonds suit - typically red in color
    Diamonds,
}

impl CardSuit {
    /// Creates a card suit from its numerical index
    ///
    /// # Arguments
    /// * `suit_idx` - Numerical index (0-3) representing the suit:
    ///   - 0 = Spades
    ///   - 1 = Hearts  
    ///   - 2 = Clubs
    ///   - 3 = Diamonds
    ///
    /// # Returns
    /// Corresponding `CardSuit` variant
    ///
    /// # Note
    /// For indices outside the 0-3 range, the function uses modulo arithmetic
    /// to wrap around to a valid index (e.g., 5 becomes 1 = Hearts)
    pub fn from_index(suit_idx: CardIdx) -> Self {
        match suit_idx {
            0 => CardSuit::Spades,
            1 => CardSuit::Hearts,
            2 => CardSuit::Clubs,
            3 => CardSuit::Diamonds,
            _ => {
                let ok_suit_idx = suit_idx % 4;
                CardSuit::from_index(ok_suit_idx)
            }
        }
    }
}

/// Display implementation for human-readable suit names
impl std::fmt::Display for CardSuit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CardSuit::Spades => write!(f, "Spades"),
            CardSuit::Hearts => write!(f, "Hearts"),
            CardSuit::Clubs => write!(f, "Clubs"),
            CardSuit::Diamonds => write!(f, "Diamonds"),
        }
    }
}

/// Represents a single playing card with rank and suit
///
/// Each card has a rank (1-13 representing Ace through King) and one of four suits.
/// The struct provides methods for card creation, display, and ASCII art rendering.
#[cfg_attr(test, derive(Hash, Eq, PartialEq))]
pub struct Card {
    /// Numerical rank of the card (1 = Ace, 2-10 = number cards, 11 = Jack, 12 = Queen, 13 = King)
    pub rank: u8,
    /// Suit of the card (Spades, Hearts, Clubs, or Diamonds)
    pub suit: CardSuit,
}

impl Card {
    /// Creates a card from its numerical index in a standard deck
    ///
    /// The index encoding follows: `card_idx = (rank - 1) * 4 + suit_index`
    /// where suit_index: 0=Spades, 1=Hearts, 2=Clubs, 3=Diamonds
    ///
    /// # Arguments
    /// * `card_idx` - Index from 0 to 51 representing the card's position in a standard deck
    ///
    /// # Returns
    /// New `Card` instance with the corresponding rank and suit
    ///
    /// # Examples
    /// ```
    /// use cliard24::playing_cards::card::Card;
    ///
    /// let ace_of_spades = Card::from_index(0);  // Index 0 = Ace of Spades
    /// let king_of_diamonds = Card::from_index(51); // Index 51 = King of Diamonds
    /// ```
    ///
    /// # Note
    /// For indices beyond 51, the function uses modulo arithmetic to wrap around
    /// to a valid index within the 0-51 range
    pub fn from_index(card_idx: CardIdx) -> Self {
        // Handle indices beyond deck size using modulo arithmetic
        if card_idx >= (DECK_SIZE as u8) {
            let ok_card_idx = card_idx % (DECK_SIZE as u8);
            return Card::from_index(ok_card_idx);
        }

        // Calculate rank and suit from index
        let rank = card_idx / 4 + 1; // 1-13 (Ace-King)
        let suit_idx = card_idx % 4; // 0-3 (Spades-Diamonds)

        Card {
            rank,
            suit: CardSuit::from_index(suit_idx),
        }
    }

    /// Returns the string representation of the card's rank
    ///
    /// # Returns
    /// String slice representing the rank:
    /// - "A" for Ace (rank 1)
    /// - "2" through "10" for number cards
    /// - "J" for Jack (11), "Q" for Queen (12), "K" for King (13)
    /// - Empty string for invalid ranks
    pub fn rank_to_str(&self) -> &str {
        match self.rank {
            1 => "A",
            2 => "2",
            3 => "3",
            4 => "4",
            5 => "5",
            6 => "6",
            7 => "7",
            8 => "8",
            9 => "9",
            10 => "10",
            11 => "J",
            12 => "Q",
            13 => "K",
            _ => "", // Invalid rank (should not occur with valid CardIdx)
        }
    }

    /// Displays multiple cards as ASCII art in the terminal
    ///
    /// Renders cards side-by-side using ASCII characters to create a visual representation
    /// of each card including its rank and suit symbols.
    ///
    /// # Arguments
    /// * `cards` - Vector of cards to display
    ///
    /// # Examples
    /// ```
    /// use cliard24::playing_cards::card::Card;
    ///
    /// let cards = vec![
    ///     Card::from_index(0),  // Ace of Spades
    ///     Card::from_index(1),  // Ace of Hearts
    ///     Card::from_index(2),  // Ace of Clubs
    ///     Card::from_index(3),  // Ace of Diamonds
    /// ];
    /// Card::print_ascii_art(&cards);
    /// ```
    ///
    /// # Output Format
    /// The ASCII art consists of 6 lines per card:
    /// ```sh
    /// .------. .------. .------. .------.
    /// |A.--. | |A.--. | |A.--. | |A.--. |
    /// | :/\: | | (\/) | | :(): | | :/\: |
    /// | (__) | | :\/: | | ()() | | :\/: |
    /// | '--'A| | '--'A| | '--'A| | '--'A|
    /// `------' `------' `------' `------'
    /// ```
    pub fn print_ascii_art(cards: &[Self]) -> String {
        // Initialize 6 lines for the ASCII art (top to bottom of card)
        let mut ascii_arts: Vec<String> = vec![String::new(); 6];

        // Build each line by concatenating card representations
        for card in cards.iter() {
            // Line 0: Top border
            ascii_arts[0].push_str(" .------.");

            // Line 1: Top rank indicator with decorative elements
            ascii_arts[1].push_str(
                format!(
                    " |{}--. |",
                    format!("{:<2}", card.rank_to_str()).replace(" ", ".")
                )
                .as_str(),
            );

            // Line 2: Suit symbol (top half)
            ascii_arts[2].push_str(match card.suit {
                CardSuit::Spades => " | :/\\: |",   // Spade symbol top
                CardSuit::Hearts => " | (\\/) |",   // Heart symbol
                CardSuit::Clubs => " | :(): |",     // Club symbol top
                CardSuit::Diamonds => " | :/\\: |", // Diamond symbol top
            });

            // Line 3: Suit symbol (bottom half)
            ascii_arts[3].push_str(match card.suit {
                CardSuit::Spades => " | (__) |",    // Spade symbol bottom
                CardSuit::Hearts => " | :\\/: |",   // Heart symbol bottom
                CardSuit::Clubs => " | ()() |",     // Club symbol bottom
                CardSuit::Diamonds => " | :\\/: |", // Diamond symbol bottom
            });

            // Line 4: Bottom rank indicator
            ascii_arts[4].push_str(
                format!(
                    " | '--{}|",
                    format!("{:>2}", card.rank_to_str()).replace(" ", "'")
                )
                .as_str(),
            );

            // Line 5: Bottom border
            ascii_arts[5].push_str(" `------'");
        }

        // Print the complete ASCII art
        let all_ascii_ars = ascii_arts.join("\n");
        println!("{}", all_ascii_ars);

        all_ascii_ars
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_card_index_creation() {
        let card = Card::from_index(0);
        assert_eq!(card.rank, 1);
        assert_eq!(card.suit, CardSuit::Spades);
        let card = Card::from_index(1);
        assert_eq!(card.rank, 1);
        assert_eq!(card.suit, CardSuit::Hearts);
        let card = Card::from_index(2);
        assert_eq!(card.rank, 1);
        assert_eq!(card.suit, CardSuit::Clubs);
        let card = Card::from_index(3);
        assert_eq!(card.rank, 1);
        assert_eq!(card.suit, CardSuit::Diamonds);
        let card = Card::from_index(10);
        assert_eq!(card.rank, 3);
        assert_eq!(card.suit, CardSuit::Clubs);
        let card = Card::from_index(52);
        assert_eq!(card.rank, 1);
        assert_eq!(card.suit, CardSuit::Spades);
        let card = Card::from_index(53);
        assert_eq!(card.rank, 1);
        assert_eq!(card.suit, CardSuit::Hearts);
    }

    #[test]
    fn test_ascii_art() {
        let cards = vec![
            Card::from_index(0), // Ace of Spades
            Card::from_index(1), // Ace of Hearts
            Card::from_index(2), // Ace of Clubs
            Card::from_index(3), // Ace of Diamonds
        ];
        Card::print_ascii_art(&cards);
        assert_eq!(
            Card::print_ascii_art(&cards),
            r#" .------. .------. .------. .------.
 |A.--. | |A.--. | |A.--. | |A.--. |
 | :/\: | | (\/) | | :(): | | :/\: |
 | (__) | | :\/: | | ()() | | :\/: |
 | '--'A| | '--'A| | '--'A| | '--'A|
 `------' `------' `------' `------'"#
                .to_string()
        )
    }
}