4
\$\begingroup\$

I've been trying to learn Rust by working through Advent of Code 2023, specifically for day 7, part 1, here. It's Poker Lite:

  • You're given a list of hands. The input is formatted as [cards, bid] in the file. The cards are a string representing a hand of 5 cards.
  • Each hand can be categorised, such as "Five of a Kind", "Four of a Kind", and so on. This is a hierarchy, in the order of my Enum. If your hand is higher in the hierarchy, it automatically "wins".
  • In the case that two equivalent hands are compared, then we go through each card, in order that they are presented in the file, to determine which hand wins.
  • The hands need to be ordered in terms of rank, which is how many other hands they would beat in the sample dataset
  • The final output is to multiply the bid of a hand against the rank it has in the sorted list.

I've got my final output issue down to a single line (in play_game_1()), and I'm getting a code smell; I can see the attributes of Cards that I want to get to, but there is no obvious way to access those fields that are wrapped in an Enum.

From my research, it looks like I can use if let to access those fields. That doesn't seem right - this is perfectly deterministic, so I shouldn't be using pattern matching to get values stored in a struct? I guess I fundamentally don't understand what FiveOfKind(Cards) et al. in the Enum actually means, and I've screwed up. I created Cards to cut down on a lot of code repetition for shared fields between each enum variant.

My implementation of PartialOrd also feels cumbersome.

use std::cmp::Ordering;
use std::collections::HashMap;
use std::fs;

fn get_card_value(card: char) -> Option<u8> {
    match card {
        'A' => Some(14),
        'K' => Some(13),
        'Q' => Some(12),
        'J' => Some(11),
        'T' => Some(10),
        '9' => Some(9),
        '8' => Some(8),
        '7' => Some(7),
        '6' => Some(6),
        '5' => Some(5),
        '4' => Some(4),
        '3' => Some(3),
        '2' => Some(2),
        '1' => Some(1),
        _ => None,
    }
}

#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)]
struct Card {
    name: char,
    value: u8,
}

impl Card {
    fn new(name: char, value: u8) -> Self {
        Self {
            name: name,
            value: value,
        }
    }
}

#[derive(Debug, Ord, Eq, PartialEq)]
struct Cards {
    cards: Vec<Card>,
    bid: u64,
    ordering: u8,
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
enum Hand {
    FiveOfKind(Cards), // 7
    FourOfKind(Cards), // 6
    FullHouse(Cards),
    ThreeOfKind(Cards),
    TwoPair(Cards),
    OnePair(Cards),
    HighCard(Cards),
}

impl PartialOrd for Cards {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.ordering > other.ordering {
            return Some(Ordering::Greater);
        } else if self.ordering == other.ordering {
            for (i, card) in self.cards.iter().enumerate() {
                if card.value > other.cards[i].value {
                    return Some(Ordering::Less);
                } else if card.value == other.cards[i].value {
                    return Some(Ordering::Equal);
                } else {
                    return Some(Ordering::Greater);
                }
            }
        } else {
            return Some(Ordering::Less);
        }
        None
    }
}

fn determine_hand(deck: Vec<Card>, bid: u64) -> Option<Hand> {
    let mut counter: HashMap<char, u32> = HashMap::new();
    for card in &deck {
        *counter.entry(card.name).or_default() += 1
    }
    let max_count = *counter.values().max().unwrap();
    let mut seen_pairs = 0;
    let mut distinct_cards = 0;

    for counts in counter.values() {
        if *counts == 2 {
            seen_pairs += 1
        }
        distinct_cards += 1
    }
    if max_count == 5 {
        return Some(Hand::FiveOfKind(Cards {
            cards: deck,
            bid: bid,
            ordering: 7,
        }));
    } else if max_count == 4 {
        return Some(Hand::FourOfKind(Cards {
            cards: deck,
            bid: bid,
            ordering: 6,
        }));
    } else if max_count == 3 && distinct_cards == 2 {
        return Some(Hand::FullHouse(Cards {
            cards: deck,
            bid: bid,
            ordering: 5,
        }));
    } else if max_count == 3 && distinct_cards == 3 {
        return Some(Hand::ThreeOfKind(Cards {
            cards: deck,
            bid: bid,
            ordering: 4,
        }));
    } else if seen_pairs == 2 {
        return Some(Hand::TwoPair(Cards {
            cards: deck,
            bid: bid,
            ordering: 3,
        }));
    } else if seen_pairs == 1 && distinct_cards == 4 {
        return Some(Hand::OnePair(Cards {
            cards: deck,
            bid: bid,
            ordering: 2,
        }));
    } else if distinct_cards == 5 {
        return Some(Hand::HighCard(Cards {
            cards: deck,
            bid: bid,
            ordering: 1,
        }));
    }
    None
}

fn create_hand(hand: &[&str]) -> Hand {
    let cards = hand[0];
    let bid = hand[1].parse::<u64>().unwrap();
    let mut deck: Vec<Card> = Vec::new();
    for card in cards.chars() {
        let value = get_card_value(card).unwrap();
        deck.push(Card::new(card, value));
    }
    let hand = determine_hand(deck, bid).unwrap();
    hand
}

fn read_input(filename: &str) -> Vec<Hand> {
    let file = fs::read_to_string(filename).expect("Cannot find file");
    let rows: Vec<_> = file
        .split("\n")
        .map(|row| row.split_ascii_whitespace().collect::<Vec<&str>>())
        .collect();

    let hands = rows.iter().map(|i| create_hand(i)).collect::<Vec<_>>();
    hands
}

fn play_game_1(hands: &mut Vec<Hand>) -> () {
    hands.sort();
    let mut score = 0;
    for (rank, hand) in hands.iter().rev().enumerate() {
        println!("{:?}\n\n", hand);
        // score += rank * hand.?????;
    }
    println!("Part 1: {score}");
}

fn main() {
    let mut hands = read_input("test.txt");
    play_game_1(&mut hands);
}

The test input is:

32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$
  • You don't need to wrap your Cards inside hands, and create a field ordering for comparison, you can just compare them directly, they automatic derive for tuple also, they will simply compare them in the order they are defined. And within the tuple enum they will compare field by field.

  • For secondary comparison, you can store the cards in one single u32

u32: 0x 0000 0000
           ^ ^^^^
store them at these five half byte, that way you can compare them easily
  • Also its better to implement creation function of struct within the namespace of the impl of that struct.

  • I would also store the cards in a [u8;13] for counting the card.

  • And there are no 1 card, only A as specified in the problem.

  • You can use a const str to look up the value of card char.

My Code

use std::fs;

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy)]
enum HandType {
    HighCard,    // Smallest
    OnePair,     // |
    TwoPair,     // |
    ThreeOfKind, // |
    FullHouse,   // |
    FourOfKind,  // V
    FiveOfKind,  // Biggest  <-- this is automatic implemented when `derive(PartialOrd)`
}

#[test]
fn test_hand_type() {
    assert!(HandType::FiveOfKind > HandType::FourOfKind);
    assert!(HandType::FourOfKind > HandType::FullHouse);
    assert!(HandType::FullHouse > HandType::ThreeOfKind);
    assert!(HandType::ThreeOfKind > HandType::TwoPair);
    assert!(HandType::TwoPair > HandType::OnePair);
    assert!(HandType::OnePair > HandType::HighCard);

    // You can compare (HandType, u32)
    assert!((HandType::FiveOfKind, 0_u32) > (HandType::FourOfKind, 1000_u32));
    assert!((HandType::FourOfKind, 120_u32) > (HandType::FourOfKind, 16_u32));
}

#[derive(Debug, Eq, PartialEq)]
struct Hand {
    // for ranking hands
    ranking: (HandType, u32),
    //        ^         ^
    //        L---------+------ this will be compare first,
    //                  |       store the type of hand.
    //                  |
    //                  |
    //                  L------ this will be compare second,
    //                          store the power of the hands
    //                          0x 0000 0000
    //                                ^ ^^^^
    //                                     L each half byte store one card,
    //                                       since there are only 13 card,
    //                                       it can be represented by half byte
    //
    bid: u64,
}

impl Hand {
    const LOOK_UP: &'static str = "23456789TJQKA";

    pub fn new(cards_str: &str, bid: u64) -> Hand {
        // a counter for each kind of card
        let mut cards = [0; 13];
        // the power of the hand
        let mut x = 0;

        assert_eq!(cards_str.len(), 5);

        for card in cards_str.chars() {
            let i = Hand::get_card_value(card).unwrap();
            // inc counter
            cards[i] += 1;
            // update hands power
            x = (x << 4) + i as u32;
        }

        let hand_type = HandType::new(&cards);

        Hand {
            ranking: (hand_type, x),
            bid,
        }
    }
    fn get_card_value(card: char) -> Option<usize> {
        Hand::LOOK_UP.chars().position(|c| c==card)
    }
}

impl HandType {
    // get the hand type for comparison and sorting
    pub fn new(cards: &[u8; 13]) -> HandType {
        match cards.iter().max().unwrap() {
            5 => return HandType::FiveOfKind,
            4 => return HandType::FourOfKind,
            3 if cards.contains(&2) => return HandType::FullHouse,
            3 => return HandType::ThreeOfKind,
            _ => {}
        }
        // count number of pair
        match cards.iter().filter(|c| **c == 2).count() {
            2 => HandType::TwoPair,
            1 => HandType::OnePair,
            _ => HandType::HighCard,
        }
    }
}

fn read_input(filename: &str) -> Vec<Hand> {
    fs::read_to_string(filename)
        .expect("Cannot find file")
        .split("\n")
        .filter(|s| s.len() != 0)
        .map(|row| {
            let mut split = row.split_ascii_whitespace();
            (
                split.next().unwrap(),
                split.next().unwrap().parse::<u64>().unwrap(),
            )
        })
        .map(|(cards_str, bid)| Hand::new(cards_str, bid))
        .collect::<Vec<_>>()
}

fn main() {
    let mut hands = read_input("./test.txt");

    hands.sort_by_key(|h| h.ranking);

    let total_winning: u64 = hands
        .iter()
        .enumerate()
        .map(|(i, h)| (i as u64 + 1) * h.bid)
        .sum();

    assert_eq!(total_winning, 6440);

    println!("Total Winning : {}", total_winning);
}

Edit

Avoid Bit Manipulation

#[derive(Debug, Eq, PartialEq)]
struct Hand {
    ranking: (HandType, [u8;5]),
    bid: u64,
}

impl Hand {
    pub fn new(cards_str: &str, bid: u64) -> Hand {
        let mut cards = [0; 13];
        let mut c = [0;5];

        assert_eq!(cards_str.len(), 5);

        for (idx, card) in cards_str.chars().enumerate() {
            let i = Hand::get_card_value(card).unwrap();
            cards[i] += 1;
            c[idx] = i as u8;
        }

        let hand_type = HandType::new(&cards);

        Hand {
            ranking: (hand_type, c),
            bid,
        }
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ I certainly prefer that the enum definition itself will handle the ordering but I'm less sure about storing card values in those bytes. It might be efficient but are you sure that's not overly obfuscated? \$\endgroup\$ Commented Oct 9, 2024 at 8:47
  • 1
    \$\begingroup\$ That's preference, if you don't feel safe with bit manipulation, you can use ranking: (HandType, [u8;5]), the same thing still apply, it also compare element one at a time. I added a snippet for that \$\endgroup\$ Commented Oct 9, 2024 at 8:57

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.