2
\$\begingroup\$

I'm learning rust by creating a simple card game program, with the usual stuff, ranks, suits etc. Currently I only implemented the card generation and a simple render function (not for future use).

Looking at it, I can abstract some things, in terms of modules:

  • I have a deck generator, it can be for a poker deck with or without jokers (52 or 54 cards), can be a deck with more than two colors, and the list goes on, depending on the game. So I need some type of abstraction, which receives a deck type name, or a enum. Another approach would be to have them defined in a external file, perhaps a CSV. But I don't know if this is a good approach.
  • The render logic cannot mess with the deck generation logic. The deck generator only generates decks and the renderer only renders it.
  • The same thing happens with game logic. It needs to be in a separate module. There are N ways to play a poker game. Game logic doesn't care about render and deck generation.

My question is how it would be approached in Rust. Would I need to create different crates?

Thanks in advance and any errors or bad practices in the code below, I would like to know.

fn main() {
    let deck = build_poker_deck(true);
    for c in build_poker_deck(true) {
        render_card(&c);
    }

    println!("Generated {} cards.", deck.len()); 
}

static WHITE_SUITS: [Suit; 2] = [Suit::DIAMONDS, Suit::HEARTS];
static BLACK_SUITS: [Suit; 2] = [Suit::CLUBS, Suit::SPADES];
static RANKS: [Rank; 13] = [Rank::ACE, Rank::TWO, Rank::THREE, Rank::FOUR, Rank::FIVE, Rank::SIX, Rank::SEVEN, Rank::EIGHT, Rank::NINE, Rank::TEN, Rank::JACK, Rank::QUEEN, Rank::KING]; 

fn build_poker_deck(include_jokers: bool) -> Vec<Card> {
    let mut deck: Vec<Card> = Vec::with_capacity(54);

    for s in WHITE_SUITS.iter() {
        for r in RANKS.iter() {
            deck.push(Card {
                suit: *s,
                rank: *r,
                color: 0
            });
        }
    }

    for s in BLACK_SUITS.iter() {
        for r in RANKS.iter() {
            deck.push(Card {
                suit: *s,
                rank: *r,
                color: 1
            });
        }
    }

    if include_jokers {
        deck.push(Card {
            suit: Suit::JOKER,
            rank: Rank::JOKER,
            color: 0,
        });
        deck.push(Card {
            suit: Suit::JOKER,
            rank: Rank::JOKER,
            color: 1,
        });
    }

    deck
}

#[derive(Debug, Copy, Clone)]
enum Suit {
    DIAMONDS = 0,
    CLUBS    = 1,
    HEARTS   = 2,
    SPADES   = 3,
    JOKER    = 4,
}

#[derive(Debug, Copy, Clone)]
enum Rank {
    ACE   = 0,
    TWO   = 1,
    THREE = 2,
    FOUR  = 3,
    FIVE  = 4,
    SIX   = 5,
    SEVEN = 6,
    EIGHT = 7,
    NINE  = 8,
    TEN   = 10,
    JACK  = 11,
    QUEEN = 12,
    KING  = 13,
    JOKER = 14,
}

struct Card {
    suit: Suit,
    rank: Rank,
    color: u8,
}

fn render_card(card: &Card) {
    let rendered_suit = if card.color == 0 {
        match card.suit {
            Suit::DIAMONDS => '♢',
            Suit::CLUBS =>    '♧',
            Suit::HEARTS =>   '♡',
            Suit::SPADES =>   '♤',
            Suit::JOKER  =>   '☆',
        }
    } else {
        match card.suit {
            Suit::DIAMONDS => '♦',
            Suit::CLUBS =>    '♣',
            Suit::HEARTS =>   '♥',
            Suit::SPADES =>   '♠',
            Suit::JOKER  =>   '★',
        }
    };

    let rendered_rank = match card.rank {
        Rank::ACE => "A",
        Rank::TWO => "2",
        Rank::THREE => "3",
        Rank::FOUR => "4",
        Rank::FIVE => "5",
        Rank::SIX => "6",
        Rank::SEVEN => "7",
        Rank::EIGHT => "8",
        Rank::NINE => "9",
        Rank::TEN => "10",
        Rank::JACK => "J",
        Rank::QUEEN => "Q",
        Rank::KING => "K",
        Rank::JOKER => "J",
    };

    println!("┌────┐");
    if rendered_rank.len() > 1 {
        println!("│{} {}│", rendered_suit, rendered_rank);
    } else {
        println!("│{}  {}│", rendered_suit, rendered_rank);
    }
    println!("│    │");
    if rendered_rank.len() > 1 {
        println!("│{} {}│", rendered_rank, rendered_suit);
    } else {
        println!("│{}  {}│", rendered_rank, rendered_suit);
    }
    println!("└────┘");
}
```
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The principal problem of your code is that you want modularity but don't really have any:

  • You use static variable, and they are use directly in your deck generator, this need to recompile the program for each type of card, suit, color and you can't have different type of deck at the same time.
  • You want unspecified number of colors but only handle two when printing the deck
use std::fmt;

const WHITE_SUITS: [Suit; 2] = [
    Suit::new(SuitKind::DIAMONDS, '♦'),
    Suit::new(SuitKind::HEARTS, '♥'),
];

const BLACK_SUITS: [Suit; 2] = [
    Suit::new(SuitKind::CLUBS, '♧'),
    Suit::new(SuitKind::SPADES, '♤'),
];

const RANKS: [Rank; 13] = [
    Rank::ACE,
    Rank::TWO,
    Rank::THREE,
    Rank::FOUR,
    Rank::FIVE,
    Rank::SIX,
    Rank::SEVEN,
    Rank::EIGHT,
    Rank::NINE,
    Rank::TEN,
    Rank::JACK,
    Rank::QUEEN,
    Rank::KING,
];

fn main() {
    env_logger::Builder::new()
        .filter_level(log::LevelFilter::Info)
        .init();

    let deck = Deck::new(
        [
            (WHITE_SUITS.iter().copied(), Some('★')),
            (BLACK_SUITS.iter().copied(), Some('☆')),
        ]
        .iter()
        .cloned(),
        RANKS.iter().copied(),
    );
    println!("{}", deck);
}

struct Card {
    suit: Suit,
    rank: Rank,
}

#[derive(Debug, Copy, Clone)]
enum Rank {
    ACE = 0,
    TWO = 1,
    THREE = 2,
    FOUR = 3,
    FIVE = 4,
    SIX = 5,
    SEVEN = 6,
    EIGHT = 7,
    NINE = 8,
    TEN = 10,
    JACK = 11,
    QUEEN = 12,
    KING = 13,
    JOKER = 14,
}

impl AsRef<str> for Rank {
    fn as_ref(&self) -> &'static str {
        match self {
            Rank::ACE => "A",
            Rank::TWO => "2",
            Rank::THREE => "3",
            Rank::FOUR => "4",
            Rank::FIVE => "5",
            Rank::SIX => "6",
            Rank::SEVEN => "7",
            Rank::EIGHT => "8",
            Rank::NINE => "9",
            Rank::TEN => "10",
            Rank::JACK => "J",
            Rank::QUEEN => "Q",
            Rank::KING => "K",
            Rank::JOKER => "J",
        }
    }
}

#[derive(Debug, Copy, Clone)]
enum SuitKind {
    DIAMONDS = 0,
    CLUBS = 1,
    HEARTS = 2,
    SPADES = 3,
    JOKER = 4,
}

#[derive(Debug, Copy, Clone)]
struct Suit {
    kind: SuitKind,
    repr: char,
}

impl Suit {
    const fn new(kind: SuitKind, repr: char) -> Self {
        Self { kind, repr }
    }
}

impl fmt::Display for Rank {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "{}", self.as_ref())
    }
}

struct Deck {
    deck: Vec<Card>,
}

impl Deck {
    fn new<C, S, R, I>(colors: C, ranks: R) -> Self
    where
        C: IntoIterator<Item = (S, Option<char>)>,
        S: IntoIterator<Item = Suit>,
        R: IntoIterator<Item = I::Item, IntoIter = I>,
        I: Clone + Iterator<Item = Rank>,
    {
        log::info!("Starting genering cards.");

        let ranks = ranks.into_iter();
        let mut deck: Vec<Card> = Vec::with_capacity(54);

        for (suit, joker) in colors.into_iter() {
            for s in suit.into_iter() {
                for r in ranks.clone() {
                    deck.push(Card { suit: s, rank: r });
                }
            }
            if let Some(repr) = joker {
                deck.push(Card {
                    suit: Suit::new(SuitKind::JOKER, repr),
                    rank: Rank::JOKER,
                });
            }
        }

        log::info!("Generated {} cards.", deck.len());

        Self { deck }
    }
}

impl fmt::Display for Card {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let rank = self.rank.as_ref();
        let suit = self.suit.repr;

        writeln!(f, "┌────┐")?;
        writeln!(f, "│{:1.1} {:>2.2}│", suit, rank)?;
        writeln!(f, "│    │")?;
        writeln!(f, "│{:<2.2} {:1.1}│", rank, suit)?;
        writeln!(f, "└────┘")
    }
}

impl fmt::Display for Deck {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for c in &self.deck {
            writeln!(f, "{}", c)?;
        }

        Ok(())
    }
}

This example is quite long:

  • When you want print something always implement Display, this allow to auto implementation of trait like ToString
  • Add a lot of generic still limited
  • Improve style like use format option, use some trait from std
  • Now Suit have a repr, this allow to handle any color
  • Now the deck is generate without using any const directly
  • Replace static by const
  • I left it but there is not reason in your current code to specify variant value.
  • Now joker are giving along with colors, in a form of Option<char>, this allow handle any color of a joker and allow to add joker for only a selection of color.
  • Show example of logging instead of just printing log directly on stdout (bad example because I wanted it's work on playground but setting log level in program is not the right way, it's the user that should set it)

What you could do next ?

I didn't push further because that would take a lot of time but if you want more modularity, you will need to use trait, for now, unless you add Rank variant and SuitKind variant you are limited to a specific type of deck. It hard to know in advance what type of trait you could need, you will need to see by yourself when you implement variant of card game what you could share and so use a trait to share behaviour.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.