[Beginner] - Struct Logic for Card Game

The general rule is that things appear in the same order as in the definition, so it'll look something like this:

enum Suit {
    Club,
    Diamond,
    Heart,
    Spade,
}

enum Rank{
    Value(u8),
    Jack,
    Queen,
    King,
    Ace,
}

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

struct Deck {
    cards: Vec<Card>,
}

fn main() {   
    let deck = Deck {                  // Deck is the overall type we're building
        cards: vec![                   // Dec.cards is a vector
            Card {                     // Each vector element is a Card
                rank: Rank::Value(1),  // Card.rank is a Rank
                suit: Suit::Heart,     // Card.suit is a Suit
            }, 
            Card { rank: Rank::Queen, suit: Suit::Spade },
            /* ... more cards ... */
        ],
    };
}
2 Likes

Awesome so I got it to work using this method. Thank you very much for that explanation. Now my issue is that this is not fun to look at. Is there a way that I can just put all of these data structures into a separate file and call it in the main.rs?

Sure, create a src/lib.rs file and put whatever you want in there, making sure to mark items you want accessible from main as pub (as in pub enum Suit ...). The public contents of lib.rs are then available in main.rs under the package's name. For example if your files look something like this

# Cargo.toml
[package]
name = "card-game"
# and so on...
// src/lib.rs
pub enum Suit {
    Club,
    Diamond,
    Heart,
    Spade,
}

pub enum Rank{
    Value(u8),
    Jack,
    Queen,
    King,
    Ace,
}
// and so on...

then you can access Suit and Rank from main.rs:

// src/main.rs
use card_game::Suit;

fn main() {
    let suit = Suit::Club;

    // you don't necessarily need the use statement up top
    // it's just for convenience, you can use the items "directly" as well
    let rank = card_game::Rank::Queen;
}

To be a bit more specific, creating a lib.rs file means that you now have a package with a binary crate (main.rs) and a library crate (lib.rs). That's why using lib.rs looks just like you're using an external library called card-game.

More info is available in the book: https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html

1.I got quite a few errors at compile time due to the fields of my structs being private. However I though the declaration of pub at the ( pub struct Deck) would have made all the fields public?

pub struct Deck {
    pub cards: Vec<Card>,
}
  1. So I have this being built inside the main(). But it's just so clunky and so many lines to put in there. Is there a way to do this in fewer lines?

     let deck = Deck {                  
             cards: vec![ 
                 //Heart
                 Card { rank: Rank::Value(2), suit: Suit::Heart },
                 Card { rank: Rank::Value(3), suit: Suit::Heart },
                 Card { rank: Rank::Value(4), suit: Suit::Heart },
                 Card { rank: Rank::Value(5), suit: Suit::Heart },
                 Card { rank: Rank::Value(6), suit: Suit::Heart },
                 etc.

Nope, all fields are private by default regardless of the visibility of the struct itself. In this case marking all fields pub is fine, but in libraries they are often kept private to both hide implemetation details and to restrict what users can do with them.

First, I would move this initialization logic inside lib.rs, something like

impl Deck {
    pub fn new() -> Deck {
        Deck {
            cards: vec![
                // cards here...
            ]
        }
    }
}

With this you can just call Deck::new() anywhere that you need a new deck. For filling the deck, edit: see the post below!

1 Like

Here's one way:

// In the lib
impl Default for Deck {
    fn default() -> Self {
        let mut cards = Vec::with_capacity(52);
        for &suit in &[Suit::Club, Suit::Diamond, Suit::Heart, Suit::Spade] {
            for &rank in &[Rank::Jack, Rank::Queen, Rank::King, Rank::Ace] {
                cards.push(Card { suit, rank });
            }
            for rank in (2..11).map(|value| Rank::Value(value)) {
                cards.push(Card { suit, rank });
            }
        }
        
        Deck { cards }
    }
}

// in the bin
let deck = Deck::default();

Edit: Playground

4 Likes

I'm currently on mobile so I can't really test it, but you should also be able to use Suit::*; use Rank::* to simplify the code a bit.

So I currently now have a Populated Deck. 52 cards all 4 suits. I have stem stored as a vector. Can anyone suggest a way in which I would randomly select from this deck in order to form the cards on the table or the palyers hand?

You'll want to add the rand crate to your project, and then you can use the shuffle method to reorder the cards (adapted from the example there):

impl Deck {
    pub fn shuffle(&mut self) {
        use rand::seq::SliceRandom;
        use rand::thread_rng;

        self.cards.shuffle(&mut thread_rng);
    }
}

You can then use pop() (or similar) to remove a card from the shuffled deck and store it elsewhere.

Hey so I keep getting a compiler error with the suggestion you said. So I just wanna kinda understand what this is doing. As this post is not so much about putting together a card game, but to get hands on with questions I had on Rust. I left some comments on the lines of the code. Can you possibly explain what rust is doing here?

ERROR:

    self.cards.shuffle(&mut thread_rng);
       |                            ^^^^^^^^^^^^^^^ the trait `RngCore` is not implemented 

Code:


    impl Deck {             // So this declares that it is a method belonging to Deck
        pub fn shuffle(&mut self) { // Here we are declaring a function. Can you explain what '&mut self' means?
                                                     
            use rand::seq::SliceRandom; // Both are code imported from rand
            use rand::thread_rng;             //
            self.cards.shuffle(&mut thread_rng); // This seems to be where the error occurs.
                                                                
        }
    }

Please read the pinned Forum Code Formatting and Syntax Highlighting post and update your post to use code blocks (for both your code and the error messages), so that we can more easily see what you're trying to convey. You can edit your post with the pencil icon.

2 Likes

That means shuffle is a method on Deck which takes an exclusive reference to a Deck instance, usually so that it can be modified. You could then use it like so:

let mut my_deck = Deck { /* make a new Deck somehow... */ };
my_deck.shuffle(); // `&mut my_deck` is implicitly passed to Deck::shuffle

Here, you've imported the thread_rng function from the rand crate. But then you passed a function pointer to the thread_rng function itself into self.cards.shuffle. The intended usage is more like so:

// Call the `rand::thread_rng()` function to generate a new ThreadRng instance
let mut thread_rng = rand::thread_rng();
// Call `self.cards.shuffle` with our newly created ThreadRng instance
self.cards.shuffle(&mut thread_rng);

As for what exactly self.cards.shuffle(...) is calling, the rand crate defines the SliceRandom trait, and implements it for slices. shuffle is a method for types that implement SliceRandom. It's available to you because you imported the SliceRandom trait.

It may have been a little confusing since your method is also called shuffle.

2 Likes

I thought I'd point out another option for storing hands of cards that is more compact of that matters. I've used it in the past when doing statistical analysis of bridge hands. It's probably not helpful for what you're doing, but i think it's pretty cool.

The idea is to associate each card with a single bit in a u64. Then any hand or deck can be represented in 64 bits, so long as no card is allowed to appear twice. Moreover, you can do certain operations quite efficiently, such as checking for the presence of a given card, combining two hands, etc. Even computing the high card points of a hand (in bridge) can be done in significantly fewer operations than the number of cards.

Basically the 64 bits are 16 bits per suit, and the ranks are represented by the bits in order. Since there are a few extra bits for a 52 card deck, there is some flexibility in which ranks are assigned which bits.

Thus isn't really a struct idea for representing cards, but could be encapsulated in a struct or two with a nice interface.

2 Likes

For anyone who is intrigued, I threw together a card deck crate while proctoring an exam: see bridge-deck. It even has a fun algorithm for randomly splitting a deck (at considerably lower cost than one random number per card... if it is working as designed).

2 Likes

I’m not very good at analyzing random algorithms, but If I’m reading this right, your split algorithm needs something like O(-n*(log (r)+log(1-r))) random bits, where n is the deck size and r is the portion of the deck being selected. You’re then exploiting the fact that n is smaller than the number of random bits supplied by a single step of the random number generator, which means it only needs O(-log(r) - log(1-r)) random numbers.

It also neatly avoids the bias that comes from using modulo to pick from a set that’s not a power of 2 in size. Nicely done.

@KarateJesus, since you are a beginner to programming just learning about random generation it could be a good idea to read a bit about seeding. The rand crate has the from_seed method.

Using a deterministic seed is very useful when debugging to have reproducible results. It can also be important if you have a game that you want to save/serialize to a file and want for all the loads of that file to give the same results. This all gets more complicated when there are multiple threads, or multiple players, or a need to prevent cheating. Of course, to just learn about rust and programming these are not necessary at all.

2 Likes

Alright so I've got my deck created (method: default) and then it gets shuffled (method: shuffle). I think I may be going about the logic the wrong way though. Firstly I keep getting this error when trying to implement the pop() for a vector.

Essentially the game is: player is dealt 2 cards, 3 cards are put on the table, 1 card is added to the table, then another card is added. Eventually 5 cards on the table, 2 in the players hand. Then it will determine what the highest hand is (Starting simple for now, just want to match pattern. No betting yet and just one player.) Now is it best to extract the five cards right away? Or do the 3 and then add one and then one?

fn main() {   
    //Declare the Default method (creates the deck) then the shuffle method(Randomizes the order)
    let mut deck = Deck::default();
    deck.shuffle();  
    deck.pop(), Some(3);
}
 --> src/main.rs:7:15
  |
7 |     deck.pop(), Some(3);
  |               ^ expected one of `.`, `;`, `?`, `}`, or an operator

Have you read/are you reading the Rust book? Are you following beginners' tutorials? If not, did you look for other learning resources that work better for you? I don't want to be obnoxious but it looks like to me that you are asking question after question for the same, frequent problems which are easy to answer if you familiarize yourself with the very basics of the language.

We all get many compiler errors daily, and reading, understanding, and resolving them on one's own is part of knowing a language. It should be part of the learning process as well – going to a forum immediately just because you got a compiler error is not a great use of either your time or that of others.

Not knowing method syntax, for example, is an indication of insufficient time spent/effort made learning indovidually – there's nothing deep about syntax that would need further explanation. Syntax is defined by the language, written out in the documentation, and must be followed for no better reason than the fact that otherwise the compiler doesn't understand the code.

Yeah I do frequent the beginner book as well as rustlings. But I decided this time I needed to make something and get some guidance. It makes more sense when I can get some feedback on code and then read up on the topics. Going through tutorials doesn’t seem to stick for some reason. But you are right that I should probably just investigate the syntax errors a bit more before posting.

That is invalid syntax. My personal compiler (brain) tells me, I have no idea what you're trying to do there with the comma and Some(3). It looks like a leftover from debugging, perhaps an assertion. If you remove , Some(3), it should compile.

1 Like