Kings-Eunuch/kings-eunuch/LEARNING_NOTES.md
2026-02-08 09:13:01 -05:00

13 KiB

Rust Learning Progress - King's Eunuch Project

Session 1 - Basic Card & Hand Implementation

Concepts Learned

1. Generics (The <> Angle Brackets)

What are generics? They're like templates or placeholders for types.

Think of it like a box that can hold different things:

  • Vec<Card> = a vector (list) that holds Cards
  • Vec<i32> = a vector that holds integers
  • Option<Card> = an optional value that might contain a Card
  • Result<String, Error> = a result that contains either a String or an Error

The <> tells Rust "what type goes inside this container."

2. The Option<T> Type

Option is Rust's way of saying "this might have a value, or it might not."

It's an enum with two variants:

enum Option<T> {
    Some(T),  // Has a value
    None,     // No value (like null in other languages)
}

Example from our code:

Card::str_to_card("As")  // Returns Option<Card>
// Either: Some(Card { rank: Ace, suit: Spades })
// Or:     None (if the string is invalid)

Why not just use null? Rust doesn't have null! This forces you to handle the "no value" case explicitly, preventing null pointer errors.

3. The Result<T, E> Type

Result is for operations that can succeed or fail.

It's also an enum:

enum Result<T, E> {
    Ok(T),   // Success - contains the result
    Err(E),  // Failure - contains an error
}

Example from our code:

add_cards_from_str("As Kh")  // Returns Result<(), String>
// Either: Ok(())  - all cards parsed successfully
// Or:     Err("Invalid card: Xx")  - parsing failed

4. Enums with Values (Discriminants)

You can assign numeric values to enum variants:

enum HandRank {
    HighCard = 1,
    OnePair = 2,
    TwoPair = 3,
    // ...
}

Why is this useful? When you derive PartialOrd and Ord, Rust automatically uses these numbers for comparison:

  • Flush > Straight evaluates to true (because 6 > 5)
  • FullHouse > TwoPair evaluates to true (because 7 > 3)

This makes comparing poker hands super clean!

5. Derive Macros - Auto-generating Functionality

The #[derive(...)] attribute automatically implements traits for your type:

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum HandRank { ... }

What you get:

  • Debug - Can print with println!("{:?}", hand_rank)
  • Clone - Can create copies with .clone()
  • Copy - Automatically copied (no need for .clone())
  • PartialEq, Eq - Can use == and !=
  • PartialOrd, Ord - Can use <, >, <=, >= and sort

This saves you from writing hundreds of lines of boilerplate code!

6. Ownership, Borrowing, and References - The Heart of Rust

The Big Idea: In Rust, every value has exactly ONE owner at a time. When the owner goes out of scope, the value is dropped (freed from memory).

This prevents:

  • Memory leaks (forgetting to free memory)
  • Use-after-free bugs (using memory after it's freed)
  • Data races (two threads modifying the same data)
The Three Rules of Ownership
  1. Each value has exactly one owner
  2. When the owner goes out of scope, the value is dropped
  3. You can transfer ownership (move) or lend it out (borrow)
Example 1: Ownership Transfer (Move)
let cards1 = Vec::new();  // cards1 owns the vector
let cards2 = cards1;      // Ownership moves to cards2
// println!("{:?}", cards1);  // ❌ ERROR! cards1 no longer owns the vector
println!("{:?}", cards2);  // ✅ OK, cards2 owns it now

What happened? The vector moved from cards1 to cards2. cards1 is now invalid.

Example 2: Borrowing with References (&)

Instead of giving away ownership, you can lend it temporarily:

let cards = vec![card1, card2];
print_cards(&cards);      // Borrow it (lend it)
print_cards(&cards);      // Can borrow again! We still own it
println!("{:?}", cards);  // ✅ Still works! We still own cards

The & means "borrow this, don't take ownership"

Example 3: Mutable Borrowing (&mut)

Sometimes you need to modify borrowed data:

let mut hand = Hand::new();
hand.add_card(card);  // &mut self - borrows hand mutably
// hand can be modified, then returned

The &mut means "borrow this AND let me change it"

Why &[Card] in Function Parameters?
pub fn is_flush(cards: &[Card]) -> bool

Breaking this down:

  • & = borrow (don't take ownership)
  • [Card] = a slice (a view into a sequence of Cards)
  • &[Card] = "borrow a view of some cards"

Why not just cards: Vec<Card>? If you did that, the function would TAKE OWNERSHIP of the vector, and it would be destroyed when the function ends!

// Bad: Takes ownership
fn bad_flush(cards: Vec<Card>) -> bool {
    // cards is destroyed here when function ends!
}

let my_cards = vec![card1, card2];
bad_flush(my_cards);
// my_cards is GONE! Can't use it anymore!
// Good: Borrows
fn is_flush(cards: &[Card]) -> bool {
    // Just looking at the cards, not taking them
}

let my_cards = vec![card1, card2];
is_flush(&my_cards);  // Lend them temporarily
// my_cards still exists! Can use it again!
The Borrowing Rules
  1. Many readers OR one writer - You can have:

    • Multiple & (immutable borrows) at the same time, OR
    • ONE &mut (mutable borrow) at a time
    • But NEVER both at the same time
  2. References must always be valid - You can't borrow something that's been destroyed

Common Patterns You've Already Used

Pattern 1: Borrowing in a loop

for card in &hand.cards {  // Borrow each card
    println!("{:?}", card);
}
// hand.cards still exists after the loop!

Pattern 2: Methods that borrow self

pub fn add_card(&mut self, card: Card) {
    //          ^^^^^^^^ Borrow self mutably
    self.cards.push(card)
}

Pattern 3: Functions that borrow parameters

pub fn str_to_card(s: &str) -> Option<Card> {
    //                ^ Borrow the string, don't take it
}
Mental Model: Think of Ownership Like Books
  • Owning a book - It's yours, you can destroy it, give it away
  • Borrowing a book (&) - Someone lends it to you to read. You must return it.
  • Borrowing with permission to write (&mut) - Someone lends you their book and says you can write notes in it. You must return it when done.
  • Multiple people can read the same book (&), but only one person can write in it at a time (&mut)
Quick Reference
Syntax Meaning Can Modify? Can Use Original?
x Ownership Yes No (moved)
&x Immutable borrow No Yes
&mut x Mutable borrow Yes Yes (after borrow ends)

7. Closures - Anonymous Functions

What are closures? Small inline functions using |params| body syntax.

// Named function
fn double(x: i32) -> i32 {
    x * 2
}

// Equivalent closure
let double = |x| x * 2;

Common uses: With iterator methods like .map(), .filter(), .any(), .all()

// Check if any card is an Ace
cards.iter().any(|card| card.rank == Rank::Ace)

// Get only Hearts
cards.iter().filter(|card| card.suit == Suit::Hearts)

// Extract all ranks
cards.iter().map(|card| card.rank)

Pattern matching in parameters:

.any(|count| *count >= 5)   // count is &i32, dereference with *
.any(|&count| count >= 5)   // & unwraps, count is i32

8. HashMap - Key-Value Storage

What is it? Like a dictionary - maps keys to values.

use std::collections::HashMap;

let mut map = HashMap::new();
map.insert(Suit::Spades, 5);
map.insert(Suit::Hearts, 2);

// HashMap looks like:
// { Spades: 5, Hearts: 2 }

The counting pattern:

*map.entry(key).or_insert(0) += 1;

Breaking it down:

  1. map.entry(key) - Get or create entry for key
  2. .or_insert(0) - If new, start at 0
  3. *... - Dereference to get the value
  4. += 1 - Increment it

Common methods:

  • .get(&key) - Get value for key (returns Option)
  • .insert(key, value) - Add/update key-value pair
  • .values() - Get all values (ignoring keys)
  • .keys() - Get all keys (ignoring values)
  • .iter() - Iterate over (key, value) pairs

Example from our code:

let mut suit_counts = HashMap::new();
for card in cards {
    *suit_counts.entry(card.suit).or_insert(0) += 1;
}
// suit_counts now maps each Suit to how many times it appeared
suit_counts.values().any(|&count| count >= 5)  // Any suit has 5+?

9. Advanced Iterator Methods

.windows(n) - Sliding windows:

let numbers = vec![1, 2, 3, 4, 5];
for window in numbers.windows(3) {
    println!("{:?}", window);
}
// Output:
// [1, 2, 3]
// [2, 3, 4]
// [3, 4, 5]

Used in straight detection to check consecutive ranks!

.enumerate() - Get index with value:

let cards = vec!["As", "Kh", "Qd"];
for (i, card) in cards.iter().enumerate() {
    println!("Card {}: {}", i + 1, card);
}
// Output:
// Card 1: As
// Card 2: Kh
// Card 3: Qd

.collect::<HashSet<_>>() - Remove duplicates:

let ranks = vec![Seven, Eight, Seven, Nine];
let unique: HashSet<Rank> = ranks.iter().copied().collect();
// unique = {Seven, Eight, Nine}

.contains() - Check if item exists:

let ranks = vec![Ace, Two, Three];
if ranks.contains(&Rank::Ace) {
    println!("Has an ace!");
}

10. Pattern Matching Destructuring

In function parameters:

// Take reference, extract value
for &rank in window {     // rank is Rank (not &Rank)
    for &card in cards {   // card is Card (not &Card)
        if card.rank == rank {  // Clean comparison!
            hand.add_card(card);
        }
    }
}

Why & in patterns extracts:

  • &x in expression: "create reference TO x"
  • &x in pattern: "extract value FROM reference"

Think of it as "unwrapping" the reference!

When to use:

  • Destructuring: for &item in collection - cleaner code downstream
  • Explicit deref: for item in collection + *item - when you need both reference and value

Both are idiomatic Rust!

Special Achievement: Wheel Straight (A-2-3-4-5)

The wheel straight is a special case in poker where Ace acts as a LOW card:

  • Normal: A-K-Q-J-T (Ace high)
  • Wheel: A-2-3-4-5 (Ace low, 5 is the high card)

Challenge: After sorting, ranks look like [Ace(1), King(2), ..., Five(10), Four(11), Three(12), Two(13)]

The wheel [Ace, Five, Four, Three, Two] has values [1, 10, 11, 12, 13] - NOT consecutive!

Solution implemented:

  1. Check for Ace presence before loop
  2. Inside window loop, check if window[1..4] contains [Five, Four, Three, Two]
  3. Build hand with ranks 2-5 first, then add Ace LAST
  4. This ensures 5 is the "high card" in the hand, not Ace

Result: Correctly detects wheel even in 7-card scenarios like [A, 2, 3, 4, 5, K, Q]!

Code Fixed Today

Issue 1: Vec::push() returns nothing

Problem:

self.cards = self.cards.push(card)  // ❌ Won't compile

Why? push() modifies the vector in-place and returns () (nothing).

Solution:

self.cards.push(card)  // ✅ Just call it, don't assign

Issue 2: Missing return statement

Problem:

pub fn add_cards_from_str(&mut self, s: &str) -> Result<(), String> {
    for card_str in s.split_whitespace() {}  // Empty loop, no return
}

Solution:

pub fn add_cards_from_str(&mut self, s: &str) -> Result<(), String> {
    for card_str in s.split_whitespace() {
        match Card::str_to_card(card_str) {
            Some(card) => self.add_card(card),
            None => return Err(format!("Invalid card: {}", card_str)),
        }
    }
    Ok(())  // Return success if all cards parsed
}

Current Project Status

Completed ✓

  • Basic card representation (Suit, Rank, Card enums/structs)
  • Card parsing from strings (e.g., "As" → Ace of Spades)
  • Hand struct with vector of cards
  • add_card() method
  • add_cards_from_str() method with error handling
  • Created evaluation.rs module
  • HandRank enum with correct poker hand rankings
  • find_flush() - Full flush detection with best 5 cards
    • HashMap-based suit counting
    • Works with 5, 6, or 7 cards
    • Returns highest 5 cards of flush suit
    • All 5 test cases passing
  • find_straight() - Full straight detection
    • Detects 5 consecutive ranks
    • Works with 7 cards (Texas Hold'em)
    • Special handling for wheel straight (A-2-3-4-5)
    • Ace correctly placed last in wheel (5 is high card)
    • All 7 test cases passing (including 7-card wheel edge case!)

Next Steps

  • Implement pair/trips/quads detection (use HashMap for rank counting)
  • Implement full house detection (trips + pair)
  • Build main evaluate_hand() function
  • Hand comparison logic

Architecture Decisions Made

  • Building in layers: Core → Game State → Probability → Training/Bot
  • Keeping evaluation logic separate from game state
  • Designing for extensibility (both training tool AND poker bot)
  • See ARCHITECTURE.md for full plan

Teaching Mode

From now on, Claude will guide you through implementation rather than writing code for you. You'll learn by doing!

Questions to Explore

  • How do we compare hands?
  • How do we detect poker hand types (pair, flush, straight, etc.)?
  • How do we handle the full 52-card deck?
  • How do we model unknown cards for probability calculations?

Last updated: 2026-01-02