From b491fb01658fa0dee413ac8ace17cc2e728c75e3 Mon Sep 17 00:00:00 2001 From: Dane Sabo Date: Sun, 8 Feb 2026 09:13:01 -0500 Subject: [PATCH] initial commit --- kings-eunuch/.gitignore | 1 + kings-eunuch/ARCHITECTURE.md | 268 +++++++++++ kings-eunuch/CLAUDE.md | 185 ++++++++ kings-eunuch/Cargo.lock | 148 ++++++ kings-eunuch/Cargo.toml | 7 + kings-eunuch/LEARNING_NOTES.md | 470 ++++++++++++++++++++ kings-eunuch/src/evaluation/mod.rs | 0 kings-eunuch/src/game_engine/deck.rs | 60 +++ kings-eunuch/src/game_engine/mod.rs | 3 + kings-eunuch/src/hand_building/card.rs | 76 ++++ kings-eunuch/src/hand_building/detection.rs | 409 +++++++++++++++++ kings-eunuch/src/hand_building/hand.rs | 84 ++++ kings-eunuch/src/hand_building/mod.rs | 7 + kings-eunuch/src/lib.rs | 2 + kings-eunuch/src/main.rs | 30 ++ kings-eunuch/tests/deck_tests.rs | 43 ++ kings-eunuch/tests/flush_tests.rs | 54 +++ kings-eunuch/tests/hand_comparison_tests.rs | 225 ++++++++++ kings-eunuch/tests/high_card_tests.rs | 56 +++ kings-eunuch/tests/pair_hands_tests.rs | 211 +++++++++ kings-eunuch/tests/straight_flush_tests.rs | 75 ++++ kings-eunuch/tests/straight_tests.rs | 78 ++++ plan.md | 16 + 23 files changed, 2508 insertions(+) create mode 100644 kings-eunuch/.gitignore create mode 100644 kings-eunuch/ARCHITECTURE.md create mode 100644 kings-eunuch/CLAUDE.md create mode 100644 kings-eunuch/Cargo.lock create mode 100644 kings-eunuch/Cargo.toml create mode 100644 kings-eunuch/LEARNING_NOTES.md create mode 100644 kings-eunuch/src/evaluation/mod.rs create mode 100644 kings-eunuch/src/game_engine/deck.rs create mode 100644 kings-eunuch/src/game_engine/mod.rs create mode 100644 kings-eunuch/src/hand_building/card.rs create mode 100644 kings-eunuch/src/hand_building/detection.rs create mode 100644 kings-eunuch/src/hand_building/hand.rs create mode 100644 kings-eunuch/src/hand_building/mod.rs create mode 100644 kings-eunuch/src/lib.rs create mode 100644 kings-eunuch/src/main.rs create mode 100644 kings-eunuch/tests/deck_tests.rs create mode 100644 kings-eunuch/tests/flush_tests.rs create mode 100644 kings-eunuch/tests/hand_comparison_tests.rs create mode 100644 kings-eunuch/tests/high_card_tests.rs create mode 100644 kings-eunuch/tests/pair_hands_tests.rs create mode 100644 kings-eunuch/tests/straight_flush_tests.rs create mode 100644 kings-eunuch/tests/straight_tests.rs create mode 100644 plan.md diff --git a/kings-eunuch/.gitignore b/kings-eunuch/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/kings-eunuch/.gitignore @@ -0,0 +1 @@ +/target diff --git a/kings-eunuch/ARCHITECTURE.md b/kings-eunuch/ARCHITECTURE.md new file mode 100644 index 0000000..5c3ab8f --- /dev/null +++ b/kings-eunuch/ARCHITECTURE.md @@ -0,0 +1,268 @@ +# King's Eunuch - Architecture Plan + +## Vision +A poker training tool that helps players calibrate hand strength and learn game theory, eventually extensible to support a poker bot. + +## Design Principles +1. **Separation of Concerns** - Keep game rules separate from probability calculations separate from AI/training logic +2. **Extensibility** - Design for both training scenarios AND bot decision-making +3. **Testability** - Each component should be independently testable +4. **Clarity over Cleverness** - Code should be readable and educational + +--- + +## Layer 1: Core Card/Hand System ✓ COMPLETED + +**Purpose:** Represent cards and basic hand structures + +**Components:** +- `Card` - Individual playing card (rank + suit) +- `Suit` enum - Spades, Hearts, Diamonds, Clubs +- `Rank` enum - Two through Ace +- `Hand` - Collection of cards + +**Status:** ✓ Basic implementation complete +- Card parsing from strings +- Hand creation and card addition +- Error handling with `Result` types + +--- + +## Layer 2: Hand Evaluation (NEXT STEP) + +**Purpose:** Determine poker hand rankings and compare hands + +**Components Needed:** + +### 2.1 HandRank enum +```rust +enum HandRank { + HighCard, + Pair, + TwoPair, + ThreeOfAKind, + Straight, + Flush, + FullHouse, + FourOfAKind, + StraightFlush, + RoyalFlush, +} +``` + +### 2.2 HandEvaluator +**Responsibilities:** +- Analyze a hand of 5-7 cards +- Determine the best 5-card poker hand +- Return a `HandRank` + relevant cards (kickers) + +**Key Functions:** +- `evaluate_hand(cards: &[Card]) -> HandRank` +- `is_flush(cards: &[Card]) -> bool` +- `is_straight(cards: &[Card]) -> bool` +- `find_pairs(cards: &[Card]) -> Vec` +- etc. + +### 2.3 Hand Comparison +**Responsibilities:** +- Compare two evaluated hands +- Determine winner (including kicker logic) + +**Key Functions:** +- `compare_hands(hand1: &Hand, hand2: &Hand) -> Ordering` + +--- + +## Layer 3: Game State (FUTURE) + +**Purpose:** Model the state of a poker game at any point + +**Components Needed:** + +### 3.1 GameState struct +```rust +struct GameState { + // Cards + player_hole_cards: Vec, // Your 2 cards + community_cards: Vec, // 0-5 board cards + + // Game info + stage: GameStage, // Preflop, Flop, Turn, River + pot: u32, + + // Opponent modeling (for training scenarios) + num_opponents: u8, + known_opponent_cards: Vec, // Usually empty, for training +} + +enum GameStage { + Preflop, + Flop, + Turn, + River, +} +``` + +### 3.2 Deck Management +```rust +struct Deck { + cards: Vec, +} +``` +- Generate full 52-card deck +- Remove known cards +- Shuffle and deal + +--- + +## Layer 4: Probability Engine (FUTURE) + +**Purpose:** Calculate winning probabilities and equity + +**Approach:** Monte Carlo Simulation +1. Take current game state +2. Generate N random completions of unknown cards +3. Evaluate each scenario +4. Count wins/losses/ties +5. Return equity percentage + +**Components:** + +### 4.1 Simulator +```rust +struct Simulator { + known_cards: Vec, + remaining_deck: Vec, +} +``` + +**Key Functions:** +- `calculate_equity(state: &GameState, simulations: u32) -> f64` +- `run_simulation(state: &GameState) -> WinLossTie` +- `deal_random_completion(state: &GameState) -> CompleteHand` + +### 4.2 Equity Calculator +- Handle different scenarios (heads-up, multi-way) +- Account for known opponent ranges (advanced) + +--- + +## Layer 5: Training System (FUTURE) + +**Purpose:** Present scenarios and quiz users + +**Components:** + +### 5.1 Scenario Generator +```rust +struct Scenario { + description: String, + game_state: GameState, + question: Question, + correct_answer: Answer, +} + +enum Question { + WhatIsYourEquity, + ShouldYouCall { pot_odds: f64 }, + WhatIsExpectedValue { bet_size: u32 }, +} +``` + +### 5.2 Training Session +- Present scenarios +- Collect user answers +- Calculate accuracy +- Track progress over time +- Adaptive difficulty (like Duolingo) + +--- + +## Layer 6: Bot/AI (FAR FUTURE) + +**Purpose:** Make optimal poker decisions + +**Components:** +- Decision engine (using equity calculations) +- Range analysis +- GTO (Game Theory Optimal) solver +- Exploit strategies + +--- + +## Implementation Roadmap + +### Phase 1: Foundation (Current) +- [x] Basic card representation +- [x] Hand struct +- [x] Card parsing +- [ ] **Hand evaluation** ← YOU ARE HERE +- [ ] Hand comparison + +### Phase 2: Game Mechanics +- [ ] Deck implementation +- [ ] GameState struct +- [ ] Deal cards from deck +- [ ] Track game stages + +### Phase 3: Probability +- [ ] Monte Carlo simulator +- [ ] Equity calculator +- [ ] Basic probability scenarios + +### Phase 4: Training +- [ ] Scenario system +- [ ] Quiz mechanics +- [ ] User progress tracking +- [ ] CLI training interface + +### Phase 5: Advanced +- [ ] Expected value calculations +- [ ] Pot odds / implied odds +- [ ] Range analysis +- [ ] Bot decision making + +--- + +## Learning Path Notes + +**Current Rust Concepts Learned:** +- Enums and structs +- Option and Result +- Generics (the `<>` brackets) +- Pattern matching with `match` +- Borrowing with `&` +- Methods and `impl` blocks + +**Next Rust Concepts to Learn:** +- Traits (for comparison, ordering) +- Sorting and iterators +- HashMap (for counting card ranks) +- More advanced pattern matching +- Testing with `#[test]` + +--- + +## File Structure (Proposed) + +``` +src/ +├── lib.rs # Module declarations +├── main.rs # CLI entry point / testing +├── translation.rs # Card, Suit, Rank ✓ +├── hand.rs # Hand struct ✓ +├── evaluation.rs # Hand evaluation (NEXT) +├── game_state.rs # Game state modeling +├── simulator.rs # Monte Carlo simulation +├── training/ +│ ├── mod.rs +│ ├── scenario.rs # Training scenarios +│ └── session.rs # Training sessions +└── bot/ + ├── mod.rs + └── decision.rs # Bot logic +``` + +--- + +*Last updated: 2026-01-02* diff --git a/kings-eunuch/CLAUDE.md b/kings-eunuch/CLAUDE.md new file mode 100644 index 0000000..c0ee00c --- /dev/null +++ b/kings-eunuch/CLAUDE.md @@ -0,0 +1,185 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +King's Eunuch is a poker training tool designed to help users calibrate hand strength and learn poker game theory. The project is built in Rust and is designed to eventually support poker bot functionality. It's aimed at beginners and focuses on teaching through situational environments. + +## Learning Project - Tutoring Approach + +**This is a Rust learning project.** The developer is using this codebase to learn Rust programming. + +When working with this code, Claude should act as a **tutor** rather than just completing tasks: +- **Explain concepts** - When suggesting code changes, explain the Rust concepts being used +- **Guide rather than solve** - Ask clarifying questions and suggest approaches rather than immediately writing full solutions +- **Teach patterns** - Point out idiomatic Rust patterns and explain why they're preferred +- **Encourage exploration** - Suggest areas to investigate and documentation to read +- **Build on existing knowledge** - Leverage concepts the developer already knows + +### Current Rust Knowledge + +The developer is comfortable with: +- Enums and structs +- `Option` and `Result` error handling +- Generics (the `<>` brackets) +- Pattern matching with `match` +- Borrowing and references (`&`) +- Methods and `impl` blocks +- Traits (Ord, PartialOrd, Eq, PartialEq, Hash, Copy, Clone, Debug) +- Standard collections (`HashMap`, `HashSet`, `Vec`) +- Iterators and iterator methods (`.map()`, `.filter()`, `.collect()`) +- Basic testing with `#[test]` + +### Next Rust Concepts to Explore + +Topics the developer is working toward: +- Lifetimes and lifetime annotations +- More advanced trait usage (trait bounds, where clauses) +- Error handling patterns (custom error types, `?` operator best practices) +- Ownership patterns in complex scenarios +- Module system and visibility +- Performance optimization techniques +- Advanced iterator adapters and functional patterns + +## Build and Test Commands + +```bash +# Build the project +cargo build + +# Run the main demo +cargo run + +# Run all tests +cargo test + +# Run specific test suites +cargo test flush # Flush detection tests +cargo test straight # Straight detection tests +cargo test pair # Pair-related tests +cargo test straight_flush # Straight flush tests +cargo test high_card # High card tests +cargo test hand_comparison # Hand comparison logic tests + +# Build in release mode +cargo build --release + +# Check code without building +cargo check +``` + +## Architecture + +The codebase follows a layered architecture with separation of concerns: + +### Core Modules + +**`hand_building/`** - Foundation layer for card and hand representation +- `card.rs` - Defines `Card`, `Suit`, and `Rank` types with parsing logic + - `Rank` uses reverse discriminants (Ace=1, King=2...Two=13) for simpler comparison + - String parsing format: rank char + suit char (e.g., "Ah" = Ace of hearts) + - Rank chars: 2-9, T, J, Q, K, A + - Suit chars: s (spades), h (hearts), d (diamonds), c (clubs) +- `detection.rs` - Hand detection functions that identify poker hands from cards + - Each detector returns `Option>` with exactly 5 cards sorted by rank + - Functions: `find_flush`, `find_straight`, `find_pair`, `find_two_pair`, `find_trips`, `find_quads`, `find_full_house`, `find_straight_flush`, `find_high_card` + - Handles special cases like ace-low straights (A-2-3-4-5) +- `hand.rs` - `Hand` struct with `HandRank` enum and comparison logic + - `Hand::from_cards()` evaluates cards and returns the best possible hand + - Implements `Ord` trait for hand comparison (accounts for both rank and kickers) + - `HandRank` discriminants are reversed (StraightFlush=1...HighCard=9) for easier comparison + - Universal comparison via `compare_same_rank()` method + +**`evaluation/`** - Placeholder for future hand evaluation enhancements + +**`game_engine/`** - Placeholder for future game state management + +### Design Principles + +1. **Separation of Concerns** - Game rules separate from probability calculations separate from AI/training logic +2. **Extensibility** - Designed for both training scenarios AND bot decision-making +3. **Testability** - Each component is independently testable with comprehensive test coverage +4. **Clarity over Cleverness** - Code prioritizes readability and educational value + +### Card Representation Details + +The `Rank` enum uses reverse discriminants where lower numbers = better cards. This simplifies comparison logic since Rust's default `Ord` implementation works correctly: +- Ace = 1 (best) +- King = 2 +- ... +- Two = 13 (worst) + +This means you can directly compare ranks without custom comparison logic. + +### Hand Detection Pattern + +All hand detectors follow a consistent pattern: +1. Check minimum card requirements +2. Build frequency maps using `HashMap` +3. Identify the hand pattern +4. Extract the best cards for that hand +5. Add kickers sorted by rank (best first) up to 5 cards total +6. Return `Some(Vec)` or `None` + +### Future Layers (from ARCHITECTURE.md) + +**Layer 3: Game State** - Model poker game state (hole cards, community cards, stage, pot) + +**Layer 4: Probability Engine** - Monte Carlo simulation for equity calculations + +**Layer 5: Training System** - Scenario generator and quiz mechanics for user training + +**Layer 6: Bot/AI** - Decision engine with GTO solver and exploit strategies + +## Common Patterns + +### Creating Cards from Strings + +```rust +use kings_eunuch::hand_building::Card; + +let card = Card::str_to_card("Ah").unwrap(); // Ace of hearts +let cards = cards_from_str("Ah Kh Qh Jh Th")?; // Multiple cards +``` + +### Evaluating Hands + +```rust +use kings_eunuch::hand_building::Hand; + +let cards = cards_from_str("Ah Kh Qh Jh Th")?; +let hand = Hand::from_cards(&cards).unwrap(); +// hand.rank will be HandRank::StraightFlush +// hand.cards will contain the 5 best cards +``` + +### Comparing Hands + +```rust +use std::cmp::Ordering; + +let hand1 = Hand::from_cards(&cards1).unwrap(); +let hand2 = Hand::from_cards(&cards2).unwrap(); + +match hand1.cmp(&hand2) { + Ordering::Greater => println!("hand1 wins"), + Ordering::Less => println!("hand2 wins"), + Ordering::Equal => println!("tie"), +} +``` + +## Current Status + +The project is in Phase 1 (Foundation): +- ✓ Card representation complete +- ✓ Hand struct complete +- ✓ All hand detection functions implemented +- ✓ Hand comparison logic with kicker support +- ✓ Comprehensive test coverage + +Next steps involve implementing game state management (deck, dealing, game stages). + +## Known Issue + +There is currently a syntax error in `hand.rs:83` - an unclosed delimiter. This must be fixed before the code will compile. diff --git a/kings-eunuch/Cargo.lock b/kings-eunuch/Cargo.lock new file mode 100644 index 0000000..031a6b3 --- /dev/null +++ b/kings-eunuch/Cargo.lock @@ -0,0 +1,148 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "kings-eunuch" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/kings-eunuch/Cargo.toml b/kings-eunuch/Cargo.toml new file mode 100644 index 0000000..32ef5a4 --- /dev/null +++ b/kings-eunuch/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "kings-eunuch" +version = "0.1.0" +edition = "2024" + +[dependencies] +rand = "0.9.2" diff --git a/kings-eunuch/LEARNING_NOTES.md b/kings-eunuch/LEARNING_NOTES.md new file mode 100644 index 0000000..b909c74 --- /dev/null +++ b/kings-eunuch/LEARNING_NOTES.md @@ -0,0 +1,470 @@ +# 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` = a vector (list) that holds Cards +- `Vec` = a vector that holds integers +- `Option` = an optional value that might contain a Card +- `Result` = a result that contains either a String or an Error + +The `<>` tells Rust "what type goes inside this container." + +#### 2. The `Option` Type +`Option` is Rust's way of saying "this might have a value, or it might not." + +It's an enum with two variants: +```rust +enum Option { + Some(T), // Has a value + None, // No value (like null in other languages) +} +``` + +**Example from our code:** +```rust +Card::str_to_card("As") // Returns Option +// 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` Type +`Result` is for operations that can succeed or fail. + +It's also an enum: +```rust +enum Result { + Ok(T), // Success - contains the result + Err(E), // Failure - contains an error +} +``` + +**Example from our code:** +```rust +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: + +```rust +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: + +```rust +#[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) + +```rust +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: + +```rust +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: + +```rust +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? + +```rust +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`?** +If you did that, the function would TAKE OWNERSHIP of the vector, and it would be destroyed when the function ends! + +```rust +// Bad: Takes ownership +fn bad_flush(cards: Vec) -> 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! +``` + +```rust +// 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** +```rust +for card in &hand.cards { // Borrow each card + println!("{:?}", card); +} +// hand.cards still exists after the loop! +``` + +**Pattern 2: Methods that borrow self** +```rust +pub fn add_card(&mut self, card: Card) { + // ^^^^^^^^ Borrow self mutably + self.cards.push(card) +} +``` + +**Pattern 3: Functions that borrow parameters** +```rust +pub fn str_to_card(s: &str) -> Option { + // ^ 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. + +```rust +// 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()` + +```rust +// 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:** +```rust +.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. + +```rust +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:** +```rust +*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:** +```rust +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:** +```rust +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:** +```rust +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::>()` - Remove duplicates:** +```rust +let ranks = vec![Seven, Eight, Seven, Nine]; +let unique: HashSet = ranks.iter().copied().collect(); +// unique = {Seven, Eight, Nine} +``` + +**`.contains()` - Check if item exists:** +```rust +let ranks = vec![Ace, Two, Three]; +if ranks.contains(&Rank::Ace) { + println!("Has an ace!"); +} +``` + +#### 10. Pattern Matching Destructuring + +**In function parameters:** +```rust +// 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:** +```rust +self.cards = self.cards.push(card) // ❌ Won't compile +``` + +**Why?** `push()` modifies the vector in-place and returns `()` (nothing). + +**Solution:** +```rust +self.cards.push(card) // ✅ Just call it, don't assign +``` + +#### Issue 2: Missing return statement +**Problem:** +```rust +pub fn add_cards_from_str(&mut self, s: &str) -> Result<(), String> { + for card_str in s.split_whitespace() {} // Empty loop, no return +} +``` + +**Solution:** +```rust +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* diff --git a/kings-eunuch/src/evaluation/mod.rs b/kings-eunuch/src/evaluation/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/kings-eunuch/src/game_engine/deck.rs b/kings-eunuch/src/game_engine/deck.rs new file mode 100644 index 0000000..32a5508 --- /dev/null +++ b/kings-eunuch/src/game_engine/deck.rs @@ -0,0 +1,60 @@ +use crate::hand_building::{Card, Rank, Suit}; +use rand::rng; +use rand::seq::SliceRandom; + +/// A standard 52-card deck of playing cards. +#[derive(Debug, Clone)] +pub struct Deck { + cards: Vec, +} + +impl Default for Deck { + fn default() -> Self { + Self::new() + } +} + +impl Deck { + /// Creates a new unshuffled deck containing all 52 cards. + pub fn new() -> Self { + use Rank::*; + use Suit::*; + + let suits = [Spades, Hearts, Diamonds, Clubs]; + let ranks = [ + Ace, King, Queen, Jack, Ten, Nine, Eight, Seven, Six, Five, Four, Three, Two, + ]; + let cards: Vec = suits + .iter() + .flat_map(|&suit| ranks.iter().map(move |&rank| Card { rank, suit })) + .collect(); + + Deck { cards } + } + + /// Shuffles the deck in place. + pub fn shuffle_cards(&mut self) { + self.cards.shuffle(&mut rng()); + } + + /// Deals a single card from the deck. + /// Returns None if the deck is empty. + pub fn deal(&mut self) -> Card { + self.cards + .pop() + .expect("It should never be possible to deal 52 cards in a single hand.") + } + + pub fn deal_multiple(&mut self, count: usize) -> Vec { + let mut cards: Vec = vec![]; + for _ in 0..count { + cards.push(self.deal()); + } + cards + } + + /// Returns the number of cards remaining in the deck. + pub fn remaining(&self) -> usize { + self.cards.len() + } +} diff --git a/kings-eunuch/src/game_engine/mod.rs b/kings-eunuch/src/game_engine/mod.rs new file mode 100644 index 0000000..db70a99 --- /dev/null +++ b/kings-eunuch/src/game_engine/mod.rs @@ -0,0 +1,3 @@ +mod deck; + +pub use deck::*; diff --git a/kings-eunuch/src/hand_building/card.rs b/kings-eunuch/src/hand_building/card.rs new file mode 100644 index 0000000..9f3cfda --- /dev/null +++ b/kings-eunuch/src/hand_building/card.rs @@ -0,0 +1,76 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Suit { + Spades, + Hearts, + Diamonds, + Clubs, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Rank { + Two = 13, + Three = 12, + Four = 11, + Five = 10, + Six = 9, + Seven = 8, + Eight = 7, + Nine = 6, + Ten = 5, + Jack = 4, + Queen = 3, + King = 2, + Ace = 1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Card { + pub suit: Suit, + pub rank: Rank, +} + +pub fn suit_parser(c: char) -> Option { + match c { + 's' => Some(Suit::Spades), + 'h' => Some(Suit::Hearts), + 'd' => Some(Suit::Diamonds), + 'c' => Some(Suit::Clubs), + _ => None, + } +} + +pub fn rank_parser(c: char) -> Option { + match c { + '2' => Some(Rank::Two), + '3' => Some(Rank::Three), + '4' => Some(Rank::Four), + '5' => Some(Rank::Five), + '6' => Some(Rank::Six), + '7' => Some(Rank::Seven), + '8' => Some(Rank::Eight), + '9' => Some(Rank::Nine), + 'T' => Some(Rank::Ten), + 'J' => Some(Rank::Jack), + 'Q' => Some(Rank::Queen), + 'K' => Some(Rank::King), + 'A' => Some(Rank::Ace), + _ => None, + } +} + +impl Card { + pub fn str_to_card(s: &str) -> Option { + if s.len() != 2 { + return None; + } + + let mut chars = s.chars(); + let rank_char = chars.next()?; + let suit_char = chars.next()?; + + let rank = rank_parser(rank_char)?; + let suit = suit_parser(suit_char)?; + + Some(Card { rank, suit }) + } +} diff --git a/kings-eunuch/src/hand_building/detection.rs b/kings-eunuch/src/hand_building/detection.rs new file mode 100644 index 0000000..0898c54 --- /dev/null +++ b/kings-eunuch/src/hand_building/detection.rs @@ -0,0 +1,409 @@ +use crate::hand_building::{Card, Rank, Suit}; +use std::collections::{HashMap, HashSet}; + +pub fn find_flush(cards: &[Card]) -> Option> { + if cards.len() < 5 { + return None; + } + + let mut suit_counts = HashMap::new(); + + for card in cards { + *suit_counts.entry(card.suit).or_insert(0) += 1; + } + + if suit_counts.values().any(|&count| count >= 5) { + let flush_suit = suit_counts + .iter() + .max_by_key(|(_, count)| *count) + .map(|(suit, _)| *suit)?; + + let mut flush_cards: Vec = cards + .iter() + .filter(|card| card.suit == flush_suit) + .copied() + .collect(); + + flush_cards.sort_by_key(|card| card.rank); + flush_cards.truncate(5); + + Some(flush_cards) + } else { + None + } +} + +fn get_unique_sorted_ranks(cards: &[Card]) -> Vec { + let mut ranks: Vec = cards + .iter() + .map(|card| card.rank) + .collect::>() + .into_iter() + .collect(); + ranks.sort(); + ranks +} + +pub fn find_straight(cards: &[Card]) -> Option> { + if cards.len() < 5 { + return None; + } + let ranks = get_unique_sorted_ranks(cards); + let ace_present = ranks.contains(&Rank::Ace); + + if ranks.len() >= 5 { + for window in ranks.windows(5) { + let is_consecutive = window + .windows(2) + .all(|pair| pair[1] as u8 == pair[0] as u8 + 1); + + if is_consecutive { + let mut straight = Vec::new(); + for &rank in window { + for &card in cards { + if card.rank == rank && straight.len() < 5 { + straight.push(card); + break; + } + } + } + return Some(straight); + } + + if ace_present { + let is_low_ace = window[1] == Rank::Five + && window[2] == Rank::Four + && window[3] == Rank::Three + && window[4] == Rank::Two; + + if is_low_ace { + let mut straight = Vec::new(); + for &rank in &window[1..] { + for &card in cards { + if card.rank == rank { + straight.push(card); + break; + } + } + } + + for &card in cards { + if card.rank == Rank::Ace { + straight.push(card); + return Some(straight); + } + } + } + } + } + } + None +} + +pub fn find_pair(cards: &[Card]) -> Option> { + if cards.len() < 2 { + return None; + } + + let mut rank_counts = HashMap::new(); + + for card in cards { + *rank_counts.entry(card.rank).or_insert(0) += 1; + } + + if rank_counts.values().filter(|&&count| count == 2).count() == 1 { + let best_pair_rank = rank_counts + .iter() + .filter(|&(_, &count)| count == 2) + .min_by_key(|(rank, _)| *rank) + .map(|(rank, _)| *rank)?; + + let best_pair: Vec = cards + .iter() + .filter(|card| card.rank == best_pair_rank) + .copied() + .collect(); + + let non_pair_cards: Vec = cards + .iter() + .filter(|card| card.rank != best_pair_rank) + .copied() + .collect(); + + let kicker_ranks = get_unique_sorted_ranks(&non_pair_cards); + + let mut result = best_pair; + + for rank in kicker_ranks { + for &card in cards { + if rank == card.rank && card.rank != best_pair_rank { + result.push(card); + if result.len() == 5 { + return Some(result); + } + break; + } + } + } + None + } else { + None + } +} + +pub fn find_two_pair(cards: &[Card]) -> Option> { + if cards.len() < 4 { + return None; + } + + let mut rank_counts = HashMap::new(); + + for card in cards { + *rank_counts.entry(card.rank).or_insert(0) += 1; + } + + if rank_counts.values().filter(|&&count| count == 2).count() >= 2 { + let best_pair_rank = rank_counts + .iter() + .filter(|&(_, &count)| count == 2) + .min_by_key(|(rank, _)| *rank) + .map(|(rank, _)| *rank)?; + + let second_best_pair_rank = rank_counts + .iter() + .filter(|(rank, _)| **rank != best_pair_rank) + .filter(|&(_, &count)| count == 2) + .min_by_key(|(rank, _)| *rank) + .map(|(rank, _)| *rank)?; + + let best_pair: Vec = cards + .iter() + .filter(|card| card.rank == best_pair_rank) + .copied() + .collect(); + + let second_best_pair: Vec = cards + .iter() + .filter(|card| card.rank == second_best_pair_rank) + .copied() + .collect(); + + let non_pair_cards: Vec = cards + .iter() + .filter(|card| card.rank != best_pair_rank) + .filter(|card| card.rank != second_best_pair_rank) + .copied() + .collect(); + + let kicker_ranks = get_unique_sorted_ranks(&non_pair_cards); + + let mut result = best_pair; + result.extend(second_best_pair); + + for rank in kicker_ranks { + for &card in cards { + if rank == card.rank && card.rank != best_pair_rank && card.rank != second_best_pair_rank { + result.push(card); + if result.len() == 5 { + return Some(result); + } + break; + } + } + } + None + } else { + None + } +} + +pub fn find_trips(cards: &[Card]) -> Option> { + if cards.len() < 3 { + return None; + } + + let mut rank_counts = HashMap::new(); + + for card in cards { + *rank_counts.entry(card.rank).or_insert(0) += 1; + } + + if rank_counts.values().any(|&count| count == 3) + && !rank_counts.values().any(|&count| count == 2) + { + let trips_rank = rank_counts + .iter() + .filter(|&(_, &count)| count == 3) + .min_by_key(|(rank, _)| *rank) + .map(|(rank, _)| *rank)?; + + let trips: Vec = cards + .iter() + .filter(|card| card.rank == trips_rank) + .copied() + .collect(); + + let non_trips_cards: Vec = cards + .iter() + .filter(|card| card.rank != trips_rank) + .copied() + .collect(); + + let kicker_ranks = get_unique_sorted_ranks(&non_trips_cards); + + let mut result = trips; + + for rank in kicker_ranks { + for &card in cards { + if rank == card.rank && card.rank != trips_rank { + result.push(card); + if result.len() == 5 { + return Some(result); + } + break; + } + } + } + None + } else { + None + } +} + +pub fn find_quads(cards: &[Card]) -> Option> { + if cards.len() < 4 { + return None; + } + + let mut rank_counts = HashMap::new(); + + for card in cards { + *rank_counts.entry(card.rank).or_insert(0) += 1; + } + + if rank_counts.values().any(|&count| count == 4) { + let quads_rank = rank_counts + .iter() + .filter(|&(_, &count)| count == 4) + .min_by_key(|(rank, _)| *rank) + .map(|(rank, _)| *rank)?; + + let quads: Vec = cards + .iter() + .filter(|card| card.rank == quads_rank) + .copied() + .collect(); + + let non_quads_cards: Vec = cards + .iter() + .filter(|card| card.rank != quads_rank) + .copied() + .collect(); + + let kicker_ranks = get_unique_sorted_ranks(&non_quads_cards); + + let mut result = quads; + + for rank in kicker_ranks { + for &card in cards { + if rank == card.rank && card.rank != quads_rank { + result.push(card); + if result.len() == 5 { + return Some(result); + } + break; + } + } + } + None + } else { + None + } +} + +pub fn find_full_house(cards: &[Card]) -> Option> { + if cards.len() < 5 { + return None; + } + + let mut rank_counts = HashMap::new(); + + for card in cards { + *rank_counts.entry(card.rank).or_insert(0) += 1; + } + + if rank_counts.values().any(|&count| count == 3) + && rank_counts.values().any(|&count| count == 2) + { + let trips_rank = rank_counts + .iter() + .filter(|&(_, &count)| count == 3) + .min_by_key(|(rank, _)| *rank) + .map(|(rank, _)| *rank)?; + + let trips: Vec = cards + .iter() + .filter(|card| card.rank == trips_rank) + .copied() + .collect(); + + let best_pair_rank = rank_counts + .iter() + .filter(|(rank, _)| **rank != trips_rank) + .filter(|&(_, &count)| count == 2) + .min_by_key(|(rank, _)| *rank) + .map(|(rank, _)| *rank)?; + + let best_pair: Vec = cards + .iter() + .filter(|card| card.rank == best_pair_rank) + .copied() + .collect(); + + let mut result = trips; + result.extend(best_pair); + + Some(result) + } else { + None + } +} + +pub fn find_straight_flush(cards: &[Card]) -> Option> { + for sf_suit in [Suit::Clubs, Suit::Diamonds, Suit::Spades, Suit::Hearts] { + let suit_cards: Vec = cards + .iter() + .filter(|card| card.suit == sf_suit) + .copied() + .collect(); + + if suit_cards.len() >= 5 { + if let Some(straight) = find_straight(&suit_cards) { + return Some(straight); + } + } + } + None +} + +pub fn find_high_card(cards: &[Card]) -> Option> { + if cards.len() < 5 { + return None; + } + + let kicker_ranks = get_unique_sorted_ranks(cards); + let mut result = Vec::new(); + + for rank in kicker_ranks { + for &card in cards { + if rank == card.rank { + result.push(card); + if result.len() == 5 { + return Some(result); + } + break; + } + } + } + None +} diff --git a/kings-eunuch/src/hand_building/hand.rs b/kings-eunuch/src/hand_building/hand.rs new file mode 100644 index 0000000..296d345 --- /dev/null +++ b/kings-eunuch/src/hand_building/hand.rs @@ -0,0 +1,84 @@ +use crate::hand_building::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum HandRank { + HighCard = 9, + OnePair = 8, + TwoPair = 7, + Trips = 6, + Straight = 5, + Flush = 4, + FullHouse = 3, + Quads = 2, + StraightFlush = 1, +} + +/// A poker hand with its evaluated rank and the 5 cards that make it. +/// Hands always have meaning - use `Hand::from_cards()` to evaluate cards. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Hand { + pub rank: HandRank, + pub cards: Vec, +} + +impl Hand { + /// Creates a Hand directly. Prefer `from_cards()` for most uses. + /// This exists primarily for testing. + pub fn new(rank: HandRank, cards: Vec) -> Self { + Hand { rank, cards } + } + + /// Evaluates a slice of cards and returns the best possible Hand. + pub fn from_cards(cards: &[Card]) -> Option { + let detectors: [(fn(&[Card]) -> Option>, HandRank); 9] = [ + (find_straight_flush, HandRank::StraightFlush), + (find_quads, HandRank::Quads), + (find_full_house, HandRank::FullHouse), + (find_flush, HandRank::Flush), + (find_straight, HandRank::Straight), + (find_trips, HandRank::Trips), + (find_two_pair, HandRank::TwoPair), + (find_pair, HandRank::OnePair), + (find_high_card, HandRank::HighCard), + ]; + + detectors + .into_iter() + .find_map(|(detector, rank)| detector(cards).map(|cards| Hand::new(rank, cards))) + } + + // Universal comparison: works for ALL hand types! + // Cards are sorted best-first, so compare iterators and reverse + fn compare_same_rank(&self, other: &Self) -> std::cmp::Ordering { + self.cards + .iter() + .map(|c| c.rank) + .cmp(other.cards.iter().map(|c| c.rank)) + .reverse() // Reverse because lower discriminant = better card + } +} + +impl Ord for Hand { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Compare hand ranks (also reversed: lower discriminant = better hand) + match self.rank.cmp(&other.rank).reverse() { + std::cmp::Ordering::Equal => self.compare_same_rank(other), + other_ordering => other_ordering, + } + } +} + +impl PartialOrd for Hand { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Helper function to parse cards from a string like "Ah Kh Qh Jh Th" +pub fn cards_from_str(s: &str) -> Result, String> { + s.split_whitespace() + .map(|card_str| { + Card::str_to_card(card_str).ok_or_else(|| format!("Invalid card: {}", card_str)) + }) + .collect() +} diff --git a/kings-eunuch/src/hand_building/mod.rs b/kings-eunuch/src/hand_building/mod.rs new file mode 100644 index 0000000..8b84e30 --- /dev/null +++ b/kings-eunuch/src/hand_building/mod.rs @@ -0,0 +1,7 @@ +mod card; +mod detection; +mod hand; + +pub use card::*; +pub use detection::*; +pub use hand::*; diff --git a/kings-eunuch/src/lib.rs b/kings-eunuch/src/lib.rs new file mode 100644 index 0000000..8ff20d8 --- /dev/null +++ b/kings-eunuch/src/lib.rs @@ -0,0 +1,2 @@ +pub mod hand_building; +pub mod game_engine; diff --git a/kings-eunuch/src/main.rs b/kings-eunuch/src/main.rs new file mode 100644 index 0000000..6f5ca99 --- /dev/null +++ b/kings-eunuch/src/main.rs @@ -0,0 +1,30 @@ +use kings_eunuch::hand_building::{find_flush, Card}; + +fn main() { + println!("=== King's Eunuch Poker Hand Evaluator ===\n"); + println!("A poker training tool for learning hand evaluation and game theory.\n"); + println!("Run 'cargo test' to see all hand detection tests!\n"); + + // Quick demo: Flush detection + println!("--- Demo: Flush Detection ---"); + let flush_cards: Vec = ["Ah", "Kh", "Qh", "Jh", "9h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + match find_flush(&flush_cards) { + Some(cards) => { + println!("✓ Flush detected!"); + println!(" Found {} cards in the flush", cards.len()); + } + None => println!("✗ No flush found"), + } + + println!("\nFor comprehensive testing, run: cargo test"); + println!("For specific test suites:"); + println!(" cargo test flush"); + println!(" cargo test straight"); + println!(" cargo test pair"); + println!(" cargo test straight_flush"); + println!(" cargo test high_card"); +} diff --git a/kings-eunuch/tests/deck_tests.rs b/kings-eunuch/tests/deck_tests.rs new file mode 100644 index 0000000..7ea9de1 --- /dev/null +++ b/kings-eunuch/tests/deck_tests.rs @@ -0,0 +1,43 @@ +use kings_eunuch::game_engine::Deck; + +#[test] +fn test_new_deck_has_52_cards() { + let deck = Deck::new(); + assert_eq!(deck.remaining(), 52); +} + +#[test] +fn test_default_creates_52_cards() { + let deck = Deck::default(); + assert_eq!(deck.remaining(), 52); +} + +#[test] +fn test_deal_single_card() { + let mut deck = Deck::new(); + let _card = deck.deal(); + assert_eq!(deck.remaining(), 51); +} + +#[test] +fn test_deal_multiple() { + let mut deck = Deck::new(); + let cards = deck.deal_multiple(5); + assert_eq!(cards.len(), 5); + assert_eq!(deck.remaining(), 47); +} + +#[test] +fn test_shuffle_changes_order() { + // Create two decks - one shuffled, one not + let mut deck1 = Deck::new(); + let mut deck2 = Deck::new(); + deck2.shuffle_cards(); + + // Deal all cards from both and compare order + let cards1 = deck1.deal_multiple(52); + let cards2 = deck2.deal_multiple(52); + + // Probability of same order is 1/52! (astronomically small) + assert_ne!(cards1, cards2, "Shuffled deck should have different order"); +} diff --git a/kings-eunuch/tests/flush_tests.rs b/kings-eunuch/tests/flush_tests.rs new file mode 100644 index 0000000..7dd2700 --- /dev/null +++ b/kings-eunuch/tests/flush_tests.rs @@ -0,0 +1,54 @@ +use kings_eunuch::hand_building::{find_flush, Card}; + +#[test] +fn test_basic_flush() { + let flush_cards: Vec = ["Ah", "Kh", "Qh", "Jh", "9h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_flush(&flush_cards).expect("Should find flush"); + assert_eq!(cards.len(), 5, "Flush should have 5 cards"); +} + +#[test] +fn test_flush_from_7_cards() { + let seven_cards: Vec = ["Ah", "Kh", "Qh", "Jh", "9h", "8d", "2c"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_flush(&seven_cards).expect("Should find flush from 7 cards"); + assert_eq!(cards.len(), 5, "Should return best 5 cards"); +} + +#[test] +fn test_no_flush_mixed_suits() { + let no_flush: Vec = ["Ah", "Ks", "Qd", "Jc", "9h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_flush(&no_flush).is_none(), "Should not find flush in mixed suits"); +} + +#[test] +fn test_six_spades_returns_best_five() { + let six_spades: Vec = ["As", "Ks", "Qs", "Js", "9s", "2s"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_flush(&six_spades).expect("Should find flush"); + assert_eq!(cards.len(), 5, "Should return exactly 5 cards"); +} + +#[test] +fn test_four_hearts_no_flush() { + let four_hearts: Vec = ["Ah", "Kh", "Qh", "Jh", "9s"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_flush(&four_hearts).is_none(), "4 cards is not enough for flush"); +} diff --git a/kings-eunuch/tests/hand_comparison_tests.rs b/kings-eunuch/tests/hand_comparison_tests.rs new file mode 100644 index 0000000..d06617c --- /dev/null +++ b/kings-eunuch/tests/hand_comparison_tests.rs @@ -0,0 +1,225 @@ +use kings_eunuch::hand_building::{HandRank, Hand, Card}; + +// Helper function to create a Hand from card strings +fn make_evaluated_hand(rank: HandRank, cards: &[&str]) -> Hand { + let card_vec: Vec = cards + .iter() + .map(|card_str| Card::str_to_card(card_str).unwrap()) + .collect(); + Hand::new(rank, card_vec) +} + +// ===== DIFFERENT HAND RANKS ===== + +#[test] +fn test_straight_flush_beats_quads() { + let sf = make_evaluated_hand(HandRank::StraightFlush, &["9h", "8h", "7h", "6h", "5h"]); + let quads = make_evaluated_hand(HandRank::Quads, &["Ah", "Ad", "As", "Ac", "Kh"]); + + assert!(sf > quads, "Straight flush should beat quads"); + assert!(quads < sf, "Quads should lose to straight flush"); +} + +#[test] +fn test_quads_beats_full_house() { + let quads = make_evaluated_hand(HandRank::Quads, &["3h", "3d", "3s", "3c", "2h"]); + let fh = make_evaluated_hand(HandRank::FullHouse, &["Ah", "Ad", "As", "Kc", "Kh"]); + + assert!(quads > fh, "Quads should beat full house"); +} + +#[test] +fn test_full_house_beats_flush() { + let fh = make_evaluated_hand(HandRank::FullHouse, &["7h", "7d", "7s", "2c", "2h"]); + let flush = make_evaluated_hand(HandRank::Flush, &["Ah", "Kh", "Qh", "Jh", "9h"]); + + assert!(fh > flush, "Full house should beat flush"); +} + +#[test] +fn test_flush_beats_straight() { + let flush = make_evaluated_hand(HandRank::Flush, &["9h", "7h", "6h", "4h", "2h"]); + let straight = make_evaluated_hand(HandRank::Straight, &["Ah", "Kd", "Qs", "Jc", "Th"]); + + assert!(flush > straight, "Flush should beat straight"); +} + +#[test] +fn test_straight_beats_trips() { + let straight = make_evaluated_hand(HandRank::Straight, &["5h", "4d", "3s", "2c", "Ah"]); + let trips = make_evaluated_hand(HandRank::Trips, &["Ah", "Ad", "As", "Kc", "Qh"]); + + assert!(straight > trips, "Straight should beat trips"); +} + +#[test] +fn test_trips_beats_two_pair() { + let trips = make_evaluated_hand(HandRank::Trips, &["3h", "3d", "3s", "2c", "2h"]); + let two_pair = make_evaluated_hand(HandRank::TwoPair, &["Ah", "As", "Kd", "Kc", "Qh"]); + + assert!(trips > two_pair, "Trips should beat two pair"); +} + +#[test] +fn test_two_pair_beats_pair() { + let two_pair = make_evaluated_hand(HandRank::TwoPair, &["3h", "3d", "2s", "2c", "Ah"]); + let pair = make_evaluated_hand(HandRank::OnePair, &["Ah", "As", "Kd", "Qc", "Jh"]); + + assert!(two_pair > pair, "Two pair should beat one pair"); +} + +#[test] +fn test_pair_beats_high_card() { + let pair = make_evaluated_hand(HandRank::OnePair, &["2h", "2d", "3s", "4c", "5h"]); + let high_card = make_evaluated_hand(HandRank::HighCard, &["Ah", "Ks", "Qd", "Jc", "9h"]); + + assert!(pair > high_card, "One pair should beat high card"); +} + +// ===== SAME HAND RANK COMPARISONS ===== + +#[test] +fn test_straight_flush_high_card_wins() { + let nine_high = make_evaluated_hand(HandRank::StraightFlush, &["9h", "8h", "7h", "6h", "5h"]); + let eight_high = make_evaluated_hand(HandRank::StraightFlush, &["8d", "7d", "6d", "5d", "4d"]); + + assert!(nine_high > eight_high, "9-high straight flush should beat 8-high"); +} + +#[test] +fn test_quads_same_rank_kicker_matters() { + let quads_king_kicker = make_evaluated_hand(HandRank::Quads, &["Ah", "Ad", "As", "Ac", "Kh"]); + let quads_queen_kicker = make_evaluated_hand(HandRank::Quads, &["Ah", "As", "Ad", "Ac", "Qh"]); + + assert!(quads_king_kicker > quads_queen_kicker, "Aces with King kicker should beat Aces with Queen kicker"); +} + +#[test] +fn test_quads_different_rank() { + let aces = make_evaluated_hand(HandRank::Quads, &["Ah", "Ad", "As", "Ac", "2h"]); + let kings = make_evaluated_hand(HandRank::Quads, &["Kh", "Kd", "Ks", "Kc", "Ah"]); + + assert!(aces > kings, "Quad aces should beat quad kings"); +} + +#[test] +fn test_full_house_trips_matter_most() { + let aces_over_twos = make_evaluated_hand(HandRank::FullHouse, &["Ah", "Ad", "As", "2c", "2h"]); + let kings_over_aces = make_evaluated_hand(HandRank::FullHouse, &["Kh", "Kd", "Ks", "Ac", "Ah"]); + + assert!(aces_over_twos > kings_over_aces, "Aces full should beat Kings full"); +} + +#[test] +fn test_full_house_same_trips_pair_tiebreak() { + let aces_over_kings = make_evaluated_hand(HandRank::FullHouse, &["Ah", "Ad", "As", "Kc", "Kh"]); + let aces_over_queens = make_evaluated_hand(HandRank::FullHouse, &["Ah", "Ad", "As", "Qc", "Qh"]); + + assert!(aces_over_kings > aces_over_queens, "Aces over Kings should beat Aces over Queens"); +} + +#[test] +fn test_flush_high_card_comparison() { + let ace_high = make_evaluated_hand(HandRank::Flush, &["Ah", "Kh", "Qh", "Jh", "9h"]); + let ace_king_queen_jack_eight = make_evaluated_hand(HandRank::Flush, &["Ad", "Kd", "Qd", "Jd", "8d"]); + + assert!(ace_high > ace_king_queen_jack_eight, "A-K-Q-J-9 flush should beat A-K-Q-J-8 flush"); +} + +#[test] +fn test_straight_high_card_matters() { + let broadway = make_evaluated_hand(HandRank::Straight, &["Ah", "Kd", "Qs", "Jc", "Th"]); + let nine_high = make_evaluated_hand(HandRank::Straight, &["9h", "8d", "7s", "6c", "5h"]); + + assert!(broadway > nine_high, "Broadway should beat 9-high straight"); +} + +#[test] +fn test_wheel_is_lowest_straight() { + let six_high = make_evaluated_hand(HandRank::Straight, &["6h", "5d", "4s", "3c", "2h"]); + let wheel = make_evaluated_hand(HandRank::Straight, &["5h", "4d", "3s", "2c", "Ah"]); + + assert!(six_high > wheel, "6-high straight should beat wheel (5-high)"); +} + +#[test] +fn test_trips_higher_rank_wins() { + let aces = make_evaluated_hand(HandRank::Trips, &["Ah", "Ad", "As", "Kc", "Qh"]); + let kings = make_evaluated_hand(HandRank::Trips, &["Kh", "Kd", "Ks", "Ac", "Qh"]); + + assert!(aces > kings, "Trip aces should beat trip kings"); +} + +#[test] +fn test_trips_same_rank_kicker_tiebreak() { + let trips_ace_king = make_evaluated_hand(HandRank::Trips, &["7h", "7d", "7s", "Ac", "Kh"]); + let trips_ace_queen = make_evaluated_hand(HandRank::Trips, &["7h", "7d", "7s", "Ac", "Qh"]); + + assert!(trips_ace_king > trips_ace_queen, "Trips with A-K kickers should beat trips with A-Q kickers"); +} + +#[test] +fn test_two_pair_high_pair_matters_most() { + let aces_and_twos = make_evaluated_hand(HandRank::TwoPair, &["Ah", "As", "2d", "2c", "3h"]); + let kings_and_queens = make_evaluated_hand(HandRank::TwoPair, &["Kh", "Ks", "Qd", "Qc", "Ah"]); + + assert!(aces_and_twos > kings_and_queens, "Aces and twos should beat Kings and Queens"); +} + +#[test] +fn test_two_pair_same_high_pair_low_pair_matters() { + let aces_and_kings = make_evaluated_hand(HandRank::TwoPair, &["Ah", "As", "Kd", "Kc", "2h"]); + let aces_and_queens = make_evaluated_hand(HandRank::TwoPair, &["Ah", "As", "Qd", "Qc", "Kh"]); + + assert!(aces_and_kings > aces_and_queens, "Aces and Kings should beat Aces and Queens"); +} + +#[test] +fn test_two_pair_same_pairs_kicker_matters() { + let kicker_king = make_evaluated_hand(HandRank::TwoPair, &["Ah", "As", "Kd", "Kc", "Qh"]); + let kicker_jack = make_evaluated_hand(HandRank::TwoPair, &["Ah", "As", "Kd", "Kc", "Jh"]); + + assert!(kicker_king > kicker_jack, "Same two pair, King kicker should beat Jack kicker"); +} + +#[test] +fn test_pair_higher_pair_wins() { + let aces = make_evaluated_hand(HandRank::OnePair, &["Ah", "As", "Kd", "Qc", "Jh"]); + let kings = make_evaluated_hand(HandRank::OnePair, &["Kh", "Ks", "Ad", "Qc", "Jh"]); + + assert!(aces > kings, "Pair of Aces should beat pair of Kings"); +} + +#[test] +fn test_pair_same_pair_kickers_matter() { + let pair_with_akq = make_evaluated_hand(HandRank::OnePair, &["7h", "7s", "Ad", "Kc", "Qh"]); + let pair_with_akj = make_evaluated_hand(HandRank::OnePair, &["7h", "7s", "Ad", "Kc", "Jh"]); + + assert!(pair_with_akq > pair_with_akj, "Pair with A-K-Q kickers should beat pair with A-K-J kickers"); +} + +#[test] +fn test_high_card_comparison() { + let ace_high = make_evaluated_hand(HandRank::HighCard, &["Ah", "Ks", "Qd", "Jc", "9h"]); + let ace_king_queen_jack_eight = make_evaluated_hand(HandRank::HighCard, &["Ah", "Ks", "Qd", "Jc", "8h"]); + + assert!(ace_high > ace_king_queen_jack_eight, "A-K-Q-J-9 should beat A-K-Q-J-8"); +} + +#[test] +fn test_high_card_second_card_matters() { + let ace_king_high = make_evaluated_hand(HandRank::HighCard, &["Ah", "Ks", "Qd", "Jc", "9h"]); + let ace_queen_high = make_evaluated_hand(HandRank::HighCard, &["Ah", "Qs", "Jd", "Tc", "9h"]); + + assert!(ace_king_high > ace_queen_high, "A-K high should beat A-Q high"); +} + +// ===== EQUALITY TESTS ===== + +#[test] +fn test_identical_hands_are_equal() { + let hand1 = make_evaluated_hand(HandRank::Flush, &["Ah", "Kh", "Qh", "Jh", "9h"]); + let hand2 = make_evaluated_hand(HandRank::Flush, &["Ad", "Kd", "Qd", "Jd", "9d"]); + + assert_eq!(hand1.cmp(&hand2), std::cmp::Ordering::Equal, "Same rank and cards should be equal"); +} diff --git a/kings-eunuch/tests/high_card_tests.rs b/kings-eunuch/tests/high_card_tests.rs new file mode 100644 index 0000000..f055db6 --- /dev/null +++ b/kings-eunuch/tests/high_card_tests.rs @@ -0,0 +1,56 @@ +use kings_eunuch::hand_building::{find_high_card, Card, Rank}; + +#[test] +fn test_high_card_basic() { + let high_card: Vec = ["Ah", "Ks", "Qd", "Jc", "9h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let result = find_high_card(&high_card).expect("Should always find high card"); + assert_eq!(result.len(), 5, "High card should have 5 cards"); + assert_eq!(result[0].rank, Rank::Ace, "First card should be Ace"); +} + +#[test] +fn test_high_card_from_7_cards() { + let seven_cards: Vec = ["Ah", "Ks", "Qd", "Jc", "9h", "7s", "2d"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let result = find_high_card(&seven_cards).expect("Should find high card from 7 cards"); + assert_eq!(result.len(), 5, "Should return exactly 5 highest cards"); +} + +#[test] +fn test_high_card_with_duplicates() { + let with_dups: Vec = ["Ah", "As", "Kd", "Qc", "Jh", "Ts", "9d"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let result = find_high_card(&with_dups).expect("Should find high card even with pairs"); + assert_eq!(result.len(), 5); +} + +#[test] +fn test_high_card_exactly_5_cards() { + let exactly_5: Vec = ["Kh", "Qs", "Jd", "Tc", "8h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let result = find_high_card(&exactly_5).expect("Should work with exactly 5 cards"); + assert_eq!(result.len(), 5); +} + +#[test] +fn test_high_card_insufficient_cards() { + let too_few: Vec = ["Ah", "Ks", "Qd", "Jc"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_high_card(&too_few).is_none(), "Should return None with fewer than 5 cards"); +} diff --git a/kings-eunuch/tests/pair_hands_tests.rs b/kings-eunuch/tests/pair_hands_tests.rs new file mode 100644 index 0000000..79da4bd --- /dev/null +++ b/kings-eunuch/tests/pair_hands_tests.rs @@ -0,0 +1,211 @@ +use kings_eunuch::hand_building::{find_pair, find_two_pair, find_trips, find_quads, find_full_house, Card}; + +// ===== PAIR TESTS ===== + +#[test] +fn test_pair_basic() { + let pair_aces: Vec = ["Ah", "As", "Kd", "Qc", "Jh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_pair(&pair_aces).expect("Should find pair"); + assert_eq!(cards.len(), 5, "Pair hand should have 5 cards (2 pair + 3 kickers)"); +} + +#[test] +fn test_pair_from_7_cards() { + let pair_7cards: Vec = ["Kh", "Ks", "Ad", "Qc", "Jh", "Ts", "9d"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_pair(&pair_7cards).expect("Should find pair from 7 cards"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_no_pair_all_different() { + let no_pair: Vec = ["Ah", "Ks", "Qd", "Jc", "9h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_pair(&no_pair).is_none(), "All different ranks - no pair"); +} + +#[test] +fn test_two_pair_not_detected_as_pair() { + let two_pairs: Vec = ["8h", "8d", "7s", "7c", "Kh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_pair(&two_pairs).is_none(), "Two pair should not be detected as single pair"); +} + +#[test] +fn test_trips_not_detected_as_pair() { + let trips: Vec = ["7h", "7d", "7s", "Kc", "Qh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_pair(&trips).is_none(), "Trips should not be detected as pair"); +} + +// ===== TWO PAIR TESTS ===== + +#[test] +fn test_two_pair_basic() { + let two_pair_basic: Vec = ["Kh", "Ks", "8d", "8c", "Ah"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_two_pair(&two_pair_basic).expect("Should find two pair"); + assert_eq!(cards.len(), 5, "Two pair should have 5 cards (2 pairs + 1 kicker)"); +} + +#[test] +fn test_two_pair_from_7_cards() { + let two_pair_7cards: Vec = ["Ah", "As", "Kd", "Kc", "Qh", "Js", "9d"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_two_pair(&two_pair_7cards).expect("Should find two pair"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_one_pair_not_two_pair() { + let one_pair: Vec = ["Kh", "Ks", "Ad", "Qc", "Jh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_two_pair(&one_pair).is_none(), "Single pair should not be detected as two pair"); +} + +#[test] +fn test_full_house_not_two_pair() { + let full_house: Vec = ["7h", "7d", "7s", "Kc", "Kh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_two_pair(&full_house).is_none(), "Full house should not be detected as two pair"); +} + +// ===== TRIPS TESTS ===== + +#[test] +fn test_trips_basic() { + let trips_basic: Vec = ["7h", "7d", "7s", "Ac", "Kh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_trips(&trips_basic).expect("Should find trips"); + assert_eq!(cards.len(), 5, "Trips should have 5 cards (3 trips + 2 kickers)"); +} + +#[test] +fn test_trips_from_7_cards() { + let trips_7cards: Vec = ["9h", "9d", "9s", "Ac", "Kh", "Qd", "Jc"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_trips(&trips_7cards).expect("Should find trips from 7 cards"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_full_house_not_trips() { + let full_house: Vec = ["8h", "8d", "8s", "5c", "5h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_trips(&full_house).is_none(), "Full house should not be detected as trips"); +} + +// ===== QUADS TESTS ===== + +#[test] +fn test_quads_basic() { + let quads_basic: Vec = ["Ah", "Ad", "As", "Ac", "Kh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_quads(&quads_basic).expect("Should find quads"); + assert_eq!(cards.len(), 5, "Quads should have 5 cards (4 quads + 1 kicker)"); +} + +#[test] +fn test_quads_from_7_cards() { + let quads_7cards: Vec = ["3h", "3d", "3s", "3c", "Ah", "Kd", "Qc"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_quads(&quads_7cards).expect("Should find quads from 7 cards"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_trips_not_quads() { + let trips: Vec = ["Kh", "Kd", "Ks", "Ac", "Qh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_quads(&trips).is_none(), "Trips should not be detected as quads"); +} + +// ===== FULL HOUSE TESTS ===== + +#[test] +fn test_full_house_basic() { + let fh_basic: Vec = ["7h", "7d", "7s", "Kc", "Kh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_full_house(&fh_basic).expect("Should find full house"); + assert_eq!(cards.len(), 5, "Full house should have 5 cards (3 trips + 2 pair)"); +} + +#[test] +fn test_full_house_from_7_cards() { + let fh_7cards: Vec = ["Ah", "Ad", "As", "Kc", "Kh", "Qd", "Jc"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_full_house(&fh_7cards).expect("Should find full house from 7 cards"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_just_trips_not_full_house() { + let just_trips: Vec = ["9h", "9d", "9s", "Ac", "Kh"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_full_house(&just_trips).is_none(), "Just trips should not be full house"); +} + +#[test] +fn test_two_pair_not_full_house() { + let two_pair: Vec = ["Kh", "Ks", "8d", "8c", "Ah"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_full_house(&two_pair).is_none(), "Two pair should not be full house"); +} diff --git a/kings-eunuch/tests/straight_flush_tests.rs b/kings-eunuch/tests/straight_flush_tests.rs new file mode 100644 index 0000000..4aba0d1 --- /dev/null +++ b/kings-eunuch/tests/straight_flush_tests.rs @@ -0,0 +1,75 @@ +use kings_eunuch::hand_building::{find_straight_flush, Card}; + +#[test] +fn test_straight_flush_basic() { + let sf_basic: Vec = ["9h", "8h", "7h", "6h", "5h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight_flush(&sf_basic).expect("Should find straight flush"); + assert_eq!(cards.len(), 5, "Straight flush should have 5 cards"); +} + +#[test] +fn test_royal_flush() { + let royal: Vec = ["Ah", "Kh", "Qh", "Jh", "Th"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight_flush(&royal).expect("Should find royal flush"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_straight_flush_from_7_cards() { + let sf_7cards: Vec = ["9s", "8s", "7s", "6s", "5s", "Kd", "2c"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight_flush(&sf_7cards).expect("Should find straight flush from 7 cards"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_wheel_straight_flush() { + let wheel_sf: Vec = ["Ad", "2d", "3d", "4d", "5d"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight_flush(&wheel_sf).expect("Should find wheel straight flush"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_flush_but_not_straight() { + let flush_only: Vec = ["Ac", "Kc", "Qc", "Jc", "9c"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_straight_flush(&flush_only).is_none(), "Flush without straight should not match"); +} + +#[test] +fn test_straight_but_not_flush() { + let straight_only: Vec = ["9h", "8d", "7s", "6c", "5h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_straight_flush(&straight_only).is_none(), "Straight without flush should not match"); +} + +#[test] +fn test_no_straight_flush() { + let no_sf: Vec = ["Ah", "Ks", "Qd", "Jc", "9h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_straight_flush(&no_sf).is_none(), "No straight flush in random cards"); +} diff --git a/kings-eunuch/tests/straight_tests.rs b/kings-eunuch/tests/straight_tests.rs new file mode 100644 index 0000000..1eb3be9 --- /dev/null +++ b/kings-eunuch/tests/straight_tests.rs @@ -0,0 +1,78 @@ +use kings_eunuch::hand_building::{find_straight, Card, Rank}; + +#[test] +fn test_basic_straight() { + let basic_straight: Vec = ["5h", "6d", "7s", "8c", "9h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight(&basic_straight).expect("Should find straight"); + assert_eq!(cards.len(), 5, "Straight should have 5 cards"); +} + +#[test] +fn test_straight_from_7_cards() { + let seven_cards: Vec = ["5h", "6d", "7s", "8c", "9h", "Ts", "Kd"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight(&seven_cards).expect("Should find straight from 7 cards"); + assert_eq!(cards.len(), 5, "Should return exactly 5 cards"); +} + +#[test] +fn test_broadway_straight() { + let broadway: Vec = ["Th", "Jd", "Qs", "Kc", "Ah"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight(&broadway).expect("Should find Broadway straight"); + assert_eq!(cards.len(), 5); +} + +#[test] +fn test_wheel_straight_5_cards() { + let wheel: Vec = ["Ah", "2d", "3s", "4c", "5h"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight(&wheel).expect("Should find wheel straight"); + assert_eq!(cards.len(), 5); + assert_eq!(cards[4].rank, Rank::Ace, "Ace should be last in wheel"); +} + +#[test] +fn test_wheel_straight_from_7_cards() { + let wheel_7cards: Vec = ["Ah", "2d", "3s", "4c", "5h", "Kd", "Qc"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + let cards = find_straight(&wheel_7cards).expect("Should find wheel from 7 cards"); + assert_eq!(cards.len(), 5); + assert_eq!(cards[4].rank, Rank::Ace, "Ace should be last in wheel"); +} + +#[test] +fn test_no_straight_gap_in_sequence() { + let no_straight: Vec = ["5h", "6d", "7s", "9c", "Th"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_straight(&no_straight).is_none(), "Gap should prevent straight"); +} + +#[test] +fn test_duplicates_no_straight() { + let duplicates: Vec = ["7h", "7d", "8s", "8c", "9h", "9d", "Ts"] + .iter() + .map(|s| Card::str_to_card(s).unwrap()) + .collect(); + + assert!(find_straight(&duplicates).is_none(), "Only 4 unique consecutive ranks"); +} diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..c9288ac --- /dev/null +++ b/plan.md @@ -0,0 +1,16 @@ +# Kings Eunuch Project Scope + +## P roject Description +King's Eunuch is a poker training tool that will help users +better calibrate poker hands and learn poker game theory +through experience. It is aimed at beginners, and puts +people into situational environments to understand their +odds of winning in different situations. Features will also +be included to look up hand probabilities, and do basic +multi-player situational analysis. Multiple modes will be +considered from raw probabilities of winning to eventually +getting to expected value and considering stack size. + +## Plan +First things first is to make a system to process poker +hands and cards. First classes will be about this.