├── fuzz ├── .gitignore ├── fuzz_targets │ ├── fuzzer_script_1.rs │ ├── fuzzer_script_2.rs │ ├── rank_seven.rs │ ├── replay_agent.rs │ └── multi_replay_agent.rs └── Cargo.toml ├── examples ├── exports │ ├── cfr_tree.png │ └── cfr_tree.dot ├── agent_tournament.rs ├── game_simulate.rs ├── agent_battle.rs ├── cfr_hand_demo.rs └── cfr_export_demo.rs ├── .rustfmt.toml ├── src ├── arena │ ├── competition │ │ ├── mod.rs │ │ ├── sim_iterator.rs │ │ └── holdem_competition.rs │ ├── historian │ │ ├── null.rs │ │ ├── failing.rs │ │ ├── directory_historian.rs │ │ ├── mod.rs │ │ ├── fn_historian.rs │ │ └── vec.rs │ ├── agent │ │ ├── all_in.rs │ │ ├── calling.rs │ │ ├── folding.rs │ │ └── mod.rs │ ├── errors.rs │ ├── action.rs │ ├── mod.rs │ ├── test_util.rs │ └── cfr │ │ ├── gamestate_iterator_gen.rs │ │ ├── historian.rs │ │ ├── state_store.rs │ │ ├── action_generator.rs │ │ └── node.rs ├── open_hand_history │ ├── mod.rs │ ├── writer.rs │ └── serde_utils.rs ├── holdem │ ├── mod.rs │ └── starting_hand.rs ├── core │ ├── error.rs │ ├── mod.rs │ ├── card_iter.rs │ ├── flat_deck.rs │ ├── hand.rs │ ├── deck.rs │ ├── player_bit_set.rs │ └── flat_hand.rs ├── simulated_icm │ └── mod.rs └── lib.rs ├── .gitignore ├── .cargo └── config.toml ├── .github └── workflows │ ├── prod_compile.yml │ └── main.yml ├── benches ├── holdem_starting_hand.rs ├── rank.rs ├── iter.rs ├── parse.rs ├── icm_sim.rs ├── deal_deck.rs ├── monte_carlo_game.rs └── arena.rs ├── mise.lock ├── mise.toml ├── Cargo.toml └── README.md /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /examples/exports/cfr_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliottneilclark/rs-poker/HEAD/examples/exports/cfr_tree.png -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_code_in_doc_comments = true 2 | wrap_comments = true 3 | comment_width = 80 4 | normalize_comments = true 5 | normalize_doc_attributes = true 6 | -------------------------------------------------------------------------------- /src/arena/competition/mod.rs: -------------------------------------------------------------------------------- 1 | mod holdem_competition; 2 | mod sim_iterator; 3 | mod tournament; 4 | 5 | pub use holdem_competition::HoldemCompetition; 6 | pub use sim_iterator::StandardSimulationIterator; 7 | pub use tournament::{SingleTableTournament, SingleTableTournamentBuilder}; 8 | -------------------------------------------------------------------------------- /src/open_hand_history/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module provides the open hand history format handling for 2 | //! `rs_poker`. It includes parsing, serialization, and deserialization of 3 | //! hand histories in the open format. 4 | mod hand_history; 5 | mod serde_utils; 6 | mod writer; 7 | 8 | pub use hand_history::*; 9 | pub use writer::*; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | /target/ 12 | 13 | # Generated by cargo fmt 14 | *.bk 15 | 16 | .vscode 17 | 18 | perf.data* 19 | 20 | .idea 21 | /historian_out/ 22 | .DS_Store 23 | 24 | # all the output files from 25 | # mutation testing with cargo mutants 26 | /mutants.out*/ 27 | -------------------------------------------------------------------------------- /src/arena/historian/null.rs: -------------------------------------------------------------------------------- 1 | use super::Historian; 2 | 3 | pub struct NullHistorian; 4 | 5 | impl Historian for NullHistorian { 6 | fn record_action( 7 | &mut self, 8 | _id: u128, 9 | _game_state: &crate::arena::GameState, 10 | _action: crate::arena::action::Action, 11 | ) -> Result<(), super::HistorianError> { 12 | Ok(()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | # Use Native CPU 3 | # and a stack size of: 40MB 4 | rustflags = [ 5 | "-C", 6 | "target-cpu=native", 7 | "-C", 8 | "link-args=-Wl,-zstack-size=47185920", 9 | ] 10 | 11 | [target.x86_64-apple-darwin] 12 | rustflags = ["-C", "target-cpu=native"] 13 | 14 | [target.aarch64-apple-darwin] 15 | rustflags = ["-C", "target-cpu=native"] 16 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzzer_script_1.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate rs_poker; 5 | use rs_poker::holdem::RangeParser; 6 | use std::str; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | if let Ok(s) = str::from_utf8(data) { 10 | if let Ok(h) = RangeParser::parse_one(s) { 11 | assert!(!h.is_empty()); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzzer_script_2.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate rs_poker; 5 | use rs_poker::core::FlatHand; 6 | use std::str; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | if let Ok(s) = str::from_utf8(data) { 10 | if let Ok(h) = FlatHand::new_from_str(s) { 11 | assert!(s.len() >= h.len()); 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /src/holdem/mod.rs: -------------------------------------------------------------------------------- 1 | /// Module that can generate possible cards for a starting hand. 2 | mod starting_hand; 3 | /// Export `StartingHand` 4 | pub use self::starting_hand::{StartingHand, Suitedness}; 5 | 6 | /// Module for `MonteCarloGame` that holds the current state of the deck for 7 | /// simulation. 8 | mod monte_carlo_game; 9 | /// Export `MonteCarloGame` 10 | pub use self::monte_carlo_game::MonteCarloGame; 11 | 12 | /// Module with all the starting hand parsing code. 13 | mod parse; 14 | /// Export `RangeParser` 15 | pub use self::parse::RangeParser; 16 | -------------------------------------------------------------------------------- /.github/workflows/prod_compile.yml: -------------------------------------------------------------------------------- 1 | name: "Prod Compile" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | prod_compile: 14 | name: cargo prod compile 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | - uses: dtolnay/rust-toolchain@nightly 21 | with: 22 | components: rust-src 23 | - run: cargo build --release --all --all-targets -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "Test Suite" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | test: 14 | name: cargo test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | - uses: dtolnay/rust-toolchain@nightly 21 | with: 22 | components: rust-src, rustfmt, clippy 23 | - run: cargo test --all-features 24 | - run: cargo fmt --check --all --verbose 25 | - run: cargo clippy --all-targets --all-features -- -D warnings -------------------------------------------------------------------------------- /fuzz/fuzz_targets/rank_seven.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate rs_poker; 5 | use rs_poker::core::{CardIter, FlatHand, Rankable}; 6 | use std::str; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | if let Ok(s) = str::from_utf8(data) { 10 | if let Ok(h) = FlatHand::new_from_str(s) { 11 | if h.len() == 7 { 12 | let r_seven = h.rank(); 13 | let r_five_max = CardIter::new(&h[..], 5) 14 | .map(|cv| cv.rank_five()) 15 | .max() 16 | .unwrap(); 17 | assert_eq!(r_five_max, r_seven); 18 | } 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /benches/holdem_starting_hand.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate rand; 4 | extern crate rs_poker; 5 | 6 | use criterion::Criterion; 7 | use rs_poker::holdem::StartingHand; 8 | 9 | fn all_starting(c: &mut Criterion) { 10 | c.bench_function("Generate all starting hands", |b| b.iter(StartingHand::all)); 11 | } 12 | 13 | fn iter_everything(c: &mut Criterion) { 14 | c.bench_function("Iter all possible hads from all starting hands", |b| { 15 | b.iter(|| -> usize { 16 | StartingHand::all() 17 | .iter() 18 | .map(|sh| -> usize { sh.possible_hands().len() }) 19 | .sum() 20 | }) 21 | }); 22 | } 23 | 24 | criterion_group!(benches, all_starting, iter_everything); 25 | criterion_main!(benches); 26 | -------------------------------------------------------------------------------- /benches/rank.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate rand; 4 | extern crate rs_poker; 5 | 6 | use criterion::Criterion; 7 | use rs_poker::core::{Deck, FlatDeck, FlatHand, Rankable}; 8 | 9 | fn rank_one(c: &mut Criterion) { 10 | let d: FlatDeck = Deck::default().into(); 11 | let hand = FlatHand::new_with_cards(d.sample(5)); 12 | c.bench_function("Rank one 5 card hand", move |b| b.iter(|| hand.rank())); 13 | } 14 | 15 | fn rank_best_seven(c: &mut Criterion) { 16 | let d: FlatDeck = Deck::default().into(); 17 | let hand = FlatHand::new_with_cards(d.sample(7)); 18 | c.bench_function("Rank best 5card hand from 7", move |b| { 19 | b.iter(|| hand.rank()) 20 | }); 21 | } 22 | 23 | criterion_group!(benches, rank_one, rank_best_seven); 24 | criterion_main!(benches); 25 | -------------------------------------------------------------------------------- /benches/iter.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate rand; 4 | extern crate rs_poker; 5 | 6 | use criterion::Criterion; 7 | use rs_poker::core::{CardIter, FlatDeck}; 8 | 9 | fn iter_in_deck(c: &mut Criterion) { 10 | c.bench_function("Iter all 5 cards hand in deck", |b| { 11 | b.iter(|| { 12 | let d: FlatDeck = FlatDeck::default(); 13 | d.into_iter().count() 14 | }) 15 | }); 16 | } 17 | 18 | fn iter_hand(c: &mut Criterion) { 19 | let d: FlatDeck = FlatDeck::default(); 20 | let hand = d.sample(7); 21 | 22 | c.bench_function("Iter all 5 cards hand in 7 card hand ", move |b| { 23 | b.iter(|| CardIter::new(&hand[..], 5).count()) 24 | }); 25 | } 26 | 27 | criterion_group!(benches, iter_in_deck, iter_hand); 28 | criterion_main!(benches); 29 | -------------------------------------------------------------------------------- /mise.lock: -------------------------------------------------------------------------------- 1 | [[tools."cargo:cargo-nextest"]] 2 | version = "0.9.115" 3 | backend = "cargo:cargo-nextest" 4 | 5 | [[tools.rust]] 6 | version = "nightly" 7 | backend = "core:rust" 8 | 9 | [[tools.taplo]] 10 | version = "0.10.0" 11 | backend = "aqua:tamasfe/taplo" 12 | "platforms.linux-arm64" = { url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-linux-aarch64.gz"} 13 | "platforms.linux-x64" = { url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-linux-x86_64.gz"} 14 | "platforms.macos-arm64" = { url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-darwin-aarch64.gz"} 15 | "platforms.macos-x64" = { url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-darwin-x86_64.gz"} 16 | "platforms.windows-x64" = { url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-windows-x86_64.zip"} 17 | -------------------------------------------------------------------------------- /src/arena/agent/all_in.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{GameState, action::AgentAction}; 2 | 3 | use super::{Agent, AgentGenerator}; 4 | 5 | /// A simple agent that always calls. This can 6 | /// stand in for a player who is a calling 7 | /// station for the rest of a hand. 8 | #[derive(Debug, Clone, Copy, Default)] 9 | pub struct AllInAgent; 10 | 11 | impl Agent for AllInAgent { 12 | fn act(self: &mut AllInAgent, _id: u128, game_state: &GameState) -> AgentAction { 13 | AgentAction::Bet(game_state.current_player_stack() + game_state.current_round_bet()) 14 | } 15 | } 16 | 17 | /// Default `AgentGenerator` for `AllInAgent`. 18 | #[derive(Debug, Clone, Copy, Default)] 19 | pub struct AllInAgentGenerator; 20 | 21 | impl AgentGenerator for AllInAgentGenerator { 22 | fn generate(&self, _game_state: &GameState) -> Box { 23 | Box::new(AllInAgent) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/agent_tournament.rs: -------------------------------------------------------------------------------- 1 | use std::vec; 2 | 3 | use rs_poker::arena::{ 4 | AgentGenerator, 5 | agent::{CallingAgentGenerator, RandomAgentGenerator}, 6 | competition::SingleTableTournamentBuilder, 7 | }; 8 | 9 | fn main() { 10 | let stacks = vec![100.0, 100.0, 50.0]; 11 | 12 | let agent_builders: Vec> = vec![ 13 | Box::new(CallingAgentGenerator), 14 | Box::::default(), 15 | Box::::default(), 16 | ]; 17 | 18 | let game_state = 19 | rs_poker::arena::game_state::GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 20 | 21 | let tournament = SingleTableTournamentBuilder::default() 22 | .agent_generators(agent_builders) 23 | .starting_game_state(game_state) 24 | .build() 25 | .unwrap(); 26 | 27 | let results = tournament.run().unwrap(); 28 | 29 | println!("Agent Results: {results:?}"); 30 | } 31 | -------------------------------------------------------------------------------- /examples/game_simulate.rs: -------------------------------------------------------------------------------- 1 | extern crate rs_poker; 2 | use rs_poker::core::Hand; 3 | use rs_poker::holdem::MonteCarloGame; 4 | 5 | const GAMES_COUNT: i32 = 3_000_000; 6 | const STARTING_HANDS: [&str; 2] = ["Adkh", "8c8s"]; 7 | 8 | fn main() { 9 | let hands = STARTING_HANDS 10 | .iter() 11 | .map(|s| Hand::new_from_str(s).expect("Should be able to create a hand.")) 12 | .collect(); 13 | let mut g = MonteCarloGame::new(hands).expect("Should be able to create a game."); 14 | let mut wins: [u64; 2] = [0, 0]; 15 | for _ in 0..GAMES_COUNT { 16 | let r = g.simulate(); 17 | g.reset(); 18 | wins[r.0.ones().next().unwrap()] += 1 19 | } 20 | 21 | let normalized: Vec = wins 22 | .iter() 23 | .map(|cnt| *cnt as f64 / GAMES_COUNT as f64) 24 | .collect(); 25 | 26 | println!("Starting Hands =\t{STARTING_HANDS:?}"); 27 | println!("Wins =\t\t\t{wins:?}"); 28 | println!("Normalized Wins =\t{normalized:?}"); 29 | } 30 | -------------------------------------------------------------------------------- /benches/parse.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate rand; 4 | extern crate rs_poker; 5 | 6 | use criterion::Criterion; 7 | use rs_poker::holdem::RangeParser; 8 | 9 | fn parse_ako(c: &mut Criterion) { 10 | c.bench_function("Parse AKo", |b| { 11 | b.iter(|| RangeParser::parse_one("AKo")); 12 | }); 13 | } 14 | 15 | fn parse_pairs(c: &mut Criterion) { 16 | c.bench_function("Parse pairs (22+)", |b| { 17 | b.iter(|| RangeParser::parse_one("22+")); 18 | }); 19 | } 20 | 21 | fn parse_connectors(c: &mut Criterion) { 22 | c.bench_function("Parse connectors (32+)", |b| { 23 | b.iter(|| RangeParser::parse_one("32+")); 24 | }); 25 | } 26 | 27 | fn parse_plus(c: &mut Criterion) { 28 | c.bench_function("Parse plus (A2+)", |b| { 29 | b.iter(|| RangeParser::parse_one("A2+")); 30 | }); 31 | } 32 | 33 | criterion_group!( 34 | benches, 35 | parse_ako, 36 | parse_pairs, 37 | parse_connectors, 38 | parse_plus 39 | ); 40 | criterion_main!(benches); 41 | -------------------------------------------------------------------------------- /benches/icm_sim.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate rs_poker; 4 | 5 | use criterion::{Bencher, BenchmarkId, Criterion}; 6 | use rs_poker::simulated_icm::simulate_icm_tournament; 7 | 8 | use rand::{Rng, rng}; 9 | 10 | fn simulate_one_tournament(c: &mut Criterion) { 11 | let payments = vec![10_000, 6_000, 4_000, 1_000, 800]; 12 | let mut rng = rng(); 13 | let mut group = c.benchmark_group("Tournament ICM"); 14 | 15 | for num_players in [2, 3, 4, 6, 128, 256, 8000].iter() { 16 | let id = BenchmarkId::new("num_players", num_players); 17 | group.bench_with_input(id, num_players, |b: &mut Bencher, num_players: &usize| { 18 | let chips: Vec = (0..*num_players) 19 | .map(|_pn| rng.random_range(1..500)) 20 | .collect(); 21 | b.iter(|| simulate_icm_tournament(&chips, &payments)) 22 | }); 23 | } 24 | 25 | group.finish(); 26 | } 27 | 28 | criterion_group!(benches, simulate_one_tournament); 29 | criterion_main!(benches); 30 | -------------------------------------------------------------------------------- /benches/deal_deck.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate rs_poker; 4 | 5 | use rand::rng; 6 | use rs_poker::core::{Deck, FlatDeck}; 7 | 8 | fn deal_all_flat_deck(c: &mut criterion::Criterion) { 9 | let mut rng = rng(); 10 | let mut flat_deck = FlatDeck::default(); 11 | 12 | c.bench_function("deal all from FlatDeck", |b| { 13 | b.iter(|| { 14 | flat_deck.shuffle(&mut rng); 15 | while !flat_deck.is_empty() { 16 | let _card = flat_deck.deal().unwrap(); 17 | } 18 | }); 19 | }); 20 | } 21 | 22 | fn deal_all_deck(c: &mut criterion::Criterion) { 23 | let mut rng = rng(); 24 | let mut deck = Deck::default(); 25 | 26 | c.bench_function("deal all from Deck", |b| { 27 | b.iter(|| { 28 | while !deck.is_empty() { 29 | let _card = deck.deal(&mut rng).unwrap(); 30 | } 31 | }); 32 | }); 33 | } 34 | 35 | criterion_group!(benches, deal_all_flat_deck, deal_all_deck); 36 | criterion_main!(benches); 37 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | "cargo:cargo-nextest" = "latest" 3 | "rust" = { version = "nightly", profile = "default", components = "rust-src,llvm-tools,rustfmt,clippy" } 4 | taplo = "latest" 5 | 6 | [tasks.'check:fmt'] 7 | description = "Check code formatting" 8 | run = 'cargo fmt --all -- --check' 9 | 10 | [tasks.'check:clippy'] 11 | description = "Check for lints" 12 | run = 'cargo clippy --all -- -D warnings' 13 | 14 | [tasks.'check:test'] 15 | description = "Run tests with nextest" 16 | run = 'cargo nextest run --all-features --all-targets' 17 | 18 | [tasks.'check:taplo:lint'] 19 | description = "Check TOML files formatting" 20 | run = 'taplo lint $(git ls-files "*.toml")' 21 | 22 | [tasks.'check:taplo:format'] 23 | description = "Format TOML files" 24 | run = 'taplo format --check $(git ls-files "*.toml")' 25 | 26 | [tasks.check] 27 | description = "Run all checks" 28 | depends = ['check:*'] 29 | 30 | [tasks.'fix:taplo:format'] 31 | description = "Format TOML files" 32 | run = 'taplo format $(git ls-files "*.toml")' 33 | 34 | [tasks.fix] 35 | description = "Run all fixers" 36 | depends = ['fix:*'] 37 | -------------------------------------------------------------------------------- /src/core/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use super::Card; 4 | 5 | /// This is the core error type for the 6 | /// RS-Poker library. It uses `thiserror` to provide 7 | /// readable error messages 8 | #[derive(Error, Debug, Hash)] 9 | pub enum RSPokerError { 10 | #[error("Unable to parse value")] 11 | UnexpectedValueChar, 12 | #[error("Unable to parse suit")] 13 | UnexpectedSuitChar, 14 | #[error("Error reading characters while parsing")] 15 | TooFewChars, 16 | #[error("Holdem hands should never have more than 7 cards in them.")] 17 | HoldemHandSize, 18 | #[error("Card already added to hand {0}")] 19 | DuplicateCardInHand(Card), 20 | #[error("Extra un-used characters found after parsing")] 21 | UnparsedCharsRemaining, 22 | #[error("Hand range can't be offsuit while cards are suiterd")] 23 | OffSuitWithMatchingSuit, 24 | #[error("Hand range is suited while cards are not.")] 25 | SuitedWithNoMatchingSuit, 26 | #[error("Invalid use of the plus modifier")] 27 | InvalidPlusModifier, 28 | #[error("The gap between cards must be constant when defining a hand range.")] 29 | InvalidGap, 30 | #[error("Pairs can't be suited.")] 31 | InvalidSuitedPairs, 32 | } 33 | -------------------------------------------------------------------------------- /benches/monte_carlo_game.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate rs_poker; 4 | 5 | use criterion::Criterion; 6 | use rs_poker::core::Hand; 7 | use rs_poker::holdem::MonteCarloGame; 8 | 9 | fn simulate_one_monte_game(c: &mut Criterion) { 10 | let hands = ["AdAh", "2c2s"] 11 | .iter() 12 | .map(|s| Hand::new_from_str(s).expect("Should be able to create a hand.")) 13 | .collect(); 14 | let mut g = MonteCarloGame::new(hands).expect("Should be able to create a game."); 15 | 16 | c.bench_function("Simulate AdAh vs 2c2s", move |b| { 17 | b.iter(|| { 18 | let r = g.simulate(); 19 | g.reset(); 20 | r 21 | }) 22 | }); 23 | } 24 | 25 | fn simulate_unseen_hole_cards(c: &mut Criterion) { 26 | let hands = vec![Hand::new_from_str("KsKd").unwrap(), Hand::default()]; 27 | let mut g = MonteCarloGame::new(hands).expect("Should be able to create a game."); 28 | 29 | c.bench_function("Simulate KsKd vs everything", move |b| { 30 | b.iter(|| { 31 | let r = g.simulate(); 32 | g.reset(); 33 | r 34 | }) 35 | }); 36 | } 37 | 38 | criterion_group!(benches, simulate_one_monte_game, simulate_unseen_hole_cards); 39 | criterion_main!(benches); 40 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "rs_poker-fuzz" 4 | version = "0.0.2" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2021" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies.rs_poker] 13 | path = ".." 14 | features = ["arbitrary", "arena", "arena-test-util"] 15 | 16 | [dependencies] 17 | libfuzzer-sys = { version = "~0.4.9", features = ["arbitrary-derive"] } 18 | arbitrary = { version = "~1.4.1", features = ["derive"] } 19 | rand = "~0.9.1" 20 | approx = "~0.5.1" 21 | 22 | # Prevent this from interfering with workspaces 23 | [workspace] 24 | members = ["."] 25 | 26 | [profile.dev] 27 | opt-level = 2 28 | 29 | [[bin]] 30 | name = "fuzzer_script_1" 31 | path = "fuzz_targets/fuzzer_script_1.rs" 32 | test = false 33 | doc = false 34 | 35 | [[bin]] 36 | name = "fuzzer_script_2" 37 | path = "fuzz_targets/fuzzer_script_2.rs" 38 | test = false 39 | doc = false 40 | 41 | [[bin]] 42 | name = "rank_seven" 43 | path = "fuzz_targets/rank_seven.rs" 44 | test = false 45 | doc = false 46 | 47 | [[bin]] 48 | name = "replay_agent" 49 | path = "fuzz_targets/replay_agent.rs" 50 | test = false 51 | doc = false 52 | 53 | 54 | [[bin]] 55 | name = "multi_replay_agent" 56 | path = "fuzz_targets/multi_replay_agent.rs" 57 | test = false 58 | doc = false 59 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | //! This is the core module. It exports the non-holdem 2 | //! related code. 3 | 4 | mod error; 5 | pub use self::error::RSPokerError; 6 | /// card.rs has value and suit. 7 | mod card; 8 | /// Re-export Card, Value, and Suit 9 | pub use self::card::{Card, Suit, Value}; 10 | 11 | /// The bitset hand. 12 | mod hand; 13 | /// Export the hand 14 | pub use self::hand::*; 15 | /// Code related to cards in flattened hands. 16 | mod flat_hand; 17 | /// Everything in there should be public. 18 | pub use self::flat_hand::*; 19 | 20 | /// We want to be able to iterate over five card hands. 21 | mod card_iter; 22 | /// Make that functionality public. 23 | pub use self::card_iter::*; 24 | 25 | /// Deck is the normal 52 card deck. 26 | mod deck; 27 | /// Export `Deck` 28 | pub use self::deck::Deck; 29 | 30 | /// Flattened deck 31 | mod flat_deck; 32 | /// Export the trait and the result. 33 | pub use self::flat_deck::FlatDeck; 34 | 35 | /// 5 Card hand ranking code. 36 | mod rank; 37 | /// Export the trait and the results. 38 | pub use self::rank::{Rank, Rankable}; 39 | 40 | // u16 backed player set. 41 | mod player_bit_set; 42 | // u64 backed card set. 43 | mod card_bit_set; 44 | // Export the bit set and the iterator 45 | pub use self::player_bit_set::{ActivePlayerBitSetIter, PlayerBitSet}; 46 | // Export the bit set and the iterator used for cards (52 cards so u64 backed) 47 | pub use self::card_bit_set::{CardBitSet, CardBitSetIter}; 48 | -------------------------------------------------------------------------------- /src/arena/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug, PartialEq, Eq, Clone, Copy, Hash)] 4 | pub enum GameStateError { 5 | #[error("Invalid number for a bet")] 6 | BetInvalidSize, 7 | #[error("The amount bet doesn't call the previous bet")] 8 | BetSizeDoesntCall, 9 | #[error("The amount bet doesn't call our own previous bet")] 10 | BetSizeDoesntCallSelf, 11 | #[error("The raise is below the minimum raise size")] 12 | RaiseSizeTooSmall, 13 | #[error("Can't advance after showdown")] 14 | CantAdvanceRound, 15 | } 16 | 17 | #[derive(Error, Debug, PartialEq, Eq, Clone, Copy, Hash)] 18 | pub enum HoldemSimulationError { 19 | #[error("Builder needs a game state")] 20 | NeedGameState, 21 | 22 | #[error("Builder needs agents")] 23 | NeedAgents, 24 | 25 | #[error("Expected GameState to contain a winner (agent with all the money)")] 26 | NoWinner, 27 | } 28 | 29 | #[derive(Error, Debug)] 30 | pub enum ExportError { 31 | #[error("Error exporting caused by IO error")] 32 | Io(#[from] std::io::Error), 33 | 34 | #[error("Invalid export format")] 35 | InvalidExportFormat(String), 36 | 37 | #[error("Failed to run dot")] 38 | FailedToRunDot(std::process::ExitStatus), 39 | } 40 | 41 | #[derive(Error, Debug, PartialEq, Eq, Clone, Copy, Hash)] 42 | pub enum CFRStateError { 43 | #[error("Node not found at the specified index")] 44 | NodeNotFound, 45 | } 46 | -------------------------------------------------------------------------------- /src/arena/historian/failing.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{GameState, Historian}; 2 | 3 | /// A historian that will always fail to record an action 4 | /// and will return an error. 5 | /// 6 | /// This historian is useful for testing the behavior of the simulation 7 | pub struct FailingHistorian; 8 | 9 | impl Historian for FailingHistorian { 10 | fn record_action( 11 | &mut self, 12 | _id: u128, 13 | _game_state: &GameState, 14 | _action: crate::arena::action::Action, 15 | ) -> Result<(), crate::arena::historian::HistorianError> { 16 | Err(crate::arena::historian::HistorianError::UnableToRecordAction) 17 | } 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use crate::arena::{HoldemSimulationBuilder, agent::CallingAgent}; 23 | 24 | use super::*; 25 | 26 | #[test] 27 | #[should_panic] 28 | fn test_panic_fail_historian() { 29 | let historian = Box::new(FailingHistorian); 30 | 31 | let stacks = vec![100.0; 3]; 32 | let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 33 | let mut rng = rand::rng(); 34 | 35 | let mut sim = HoldemSimulationBuilder::default() 36 | .game_state(game_state) 37 | .agents(vec![ 38 | Box::new(CallingAgent {}), 39 | Box::new(CallingAgent {}), 40 | Box::new(CallingAgent {}), 41 | ]) 42 | .panic_on_historian_error(true) 43 | .historians(vec![historian]) 44 | .build() 45 | .unwrap(); 46 | 47 | // This should panic since panic_on_historian_error is set to true 48 | // and the historian will always fail to record an action 49 | sim.run(&mut rng); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/arena/agent/calling.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{action::AgentAction, game_state::GameState}; 2 | 3 | use super::{Agent, AgentGenerator}; 4 | 5 | /// A simple agent that always calls. This can 6 | /// stand in for a player who is a calling 7 | /// station for the rest of a hand. 8 | #[derive(Debug, Clone, Copy, Default)] 9 | pub struct CallingAgent; 10 | 11 | impl Agent for CallingAgent { 12 | fn act(self: &mut CallingAgent, _id: u128, game_state: &GameState) -> AgentAction { 13 | AgentAction::Bet(game_state.current_round_bet()) 14 | } 15 | } 16 | 17 | /// Default `AgentGenerator` for `CallingAgent`. 18 | #[derive(Debug, Clone, Copy, Default)] 19 | pub struct CallingAgentGenerator; 20 | 21 | impl AgentGenerator for CallingAgentGenerator { 22 | fn generate(&self, _game_state: &GameState) -> Box { 23 | Box::new(CallingAgent) 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use crate::arena::HoldemSimulationBuilder; 30 | 31 | use super::*; 32 | 33 | #[test_log::test] 34 | fn test_call_agents() { 35 | let stacks = vec![100.0; 4]; 36 | let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 37 | let mut rng = rand::rng(); 38 | let mut sim = HoldemSimulationBuilder::default() 39 | .game_state(game_state) 40 | .agents(vec![ 41 | Box::new(CallingAgent {}), 42 | Box::new(CallingAgent {}), 43 | Box::new(CallingAgent {}), 44 | Box::new(CallingAgent {}), 45 | ]) 46 | .build() 47 | .unwrap(); 48 | 49 | sim.run(&mut rng); 50 | 51 | assert_eq!(sim.game_state.num_active_players(), 4); 52 | 53 | assert_ne!(0.0, sim.game_state.player_winnings.iter().sum::()); 54 | assert_eq!(40.0, sim.game_state.player_winnings.iter().sum::()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/agent_battle.rs: -------------------------------------------------------------------------------- 1 | extern crate rs_poker; 2 | 3 | use rs_poker::arena::{ 4 | AgentGenerator, CloneHistorianGenerator, HistorianGenerator, 5 | agent::{CloneAgentGenerator, RandomAgentGenerator, RandomPotControlAgent}, 6 | competition::{HoldemCompetition, StandardSimulationIterator}, 7 | game_state::RandomGameStateGenerator, 8 | historian::DirectoryHistorian, 9 | }; 10 | 11 | const ROUNDS_BATCH: usize = 500; 12 | fn main() { 13 | // Start with very random dumb agents. 14 | let agent_gens: Vec> = vec![ 15 | Box::::default(), 16 | Box::::default(), 17 | Box::new(CloneAgentGenerator::new(RandomPotControlAgent::new(vec![ 18 | 0.5, 0.3, 19 | ]))), 20 | Box::new(CloneAgentGenerator::new(RandomPotControlAgent::new(vec![ 21 | 0.3, 0.3, 22 | ]))), 23 | ]; 24 | 25 | // Show how to use the historian to record the games. 26 | let path = std::env::current_dir().unwrap(); 27 | let dir = path.join("historian_out"); 28 | let hist_gens: Vec> = vec![Box::new(CloneHistorianGenerator::new( 29 | DirectoryHistorian::new(dir), 30 | ))]; 31 | 32 | // Run the games with completely random hands. 33 | // Starting stack of at least 10 big blinds (10x10=100 chips) 34 | // Starting stack of no more than 1000 big blinds (10x1000=10000 chips) 35 | // This isn't deep stack poker at it's finest. 36 | let game_state_gen = 37 | RandomGameStateGenerator::new(agent_gens.len(), 100.0, 10000.0, 10.0, 5.0, 0.0); 38 | let simulation_gen = StandardSimulationIterator::new(agent_gens, hist_gens, game_state_gen); 39 | let mut comp = HoldemCompetition::new(simulation_gen); 40 | for _i in 0..5000 { 41 | let _res = comp.run(ROUNDS_BATCH).expect("competition failed"); 42 | println!("Current Competition Stats: {comp:?}"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/replay_agent.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | extern crate approx; 4 | extern crate arbitrary; 5 | extern crate libfuzzer_sys; 6 | extern crate rand; 7 | extern crate rs_poker; 8 | 9 | use approx::assert_relative_ne; 10 | use rand::{rngs::StdRng, SeedableRng}; 11 | 12 | use rs_poker::arena::{ 13 | action::AgentAction, 14 | agent::VecReplayAgent, 15 | game_state::Round, 16 | historian, 17 | test_util::{assert_valid_game_state, assert_valid_history, assert_valid_round_data}, 18 | Agent, GameState, HoldemSimulation, HoldemSimulationBuilder, 19 | }; 20 | 21 | use libfuzzer_sys::fuzz_target; 22 | 23 | #[derive(Debug, Clone, arbitrary::Arbitrary)] 24 | struct Input { 25 | pub dealer_actions: Vec, 26 | pub sb_actions: Vec, 27 | pub seed: u64, 28 | } 29 | 30 | fuzz_target!(|input: Input| { 31 | let stacks = vec![50.0; 2]; 32 | let game_state = GameState::new_starting(stacks, 2.0, 1.0, 0.0, 0); 33 | let agents: Vec> = vec![ 34 | Box::::new(VecReplayAgent::new(input.dealer_actions)), 35 | Box::::new(VecReplayAgent::new(input.sb_actions)), 36 | ]; 37 | 38 | let vec_historian = Box::::new(historian::VecHistorian::new()); 39 | 40 | let storage = vec_historian.get_storage(); 41 | 42 | let historians: Vec> = vec![vec_historian]; 43 | let mut rng = StdRng::seed_from_u64(input.seed); 44 | let mut sim: HoldemSimulation = HoldemSimulationBuilder::default() 45 | .game_state(game_state) 46 | .agents(agents) 47 | .historians(historians) 48 | .build() 49 | .unwrap(); 50 | sim.run(&mut rng); 51 | 52 | assert_eq!(Round::Complete, sim.game_state.round); 53 | assert_relative_ne!(0.0_f32, sim.game_state.player_bet.iter().sum()); 54 | 55 | assert_valid_round_data(&sim.game_state.round_data); 56 | assert_valid_game_state(&sim.game_state); 57 | 58 | assert_valid_history(&storage.borrow()); 59 | }); 60 | -------------------------------------------------------------------------------- /src/arena/agent/folding.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{action::AgentAction, game_state::GameState}; 2 | 3 | use super::{Agent, AgentGenerator}; 4 | 5 | /// A simple agent that folds unless there is only one active player left. 6 | #[derive(Default, Debug, Clone, Copy)] 7 | pub struct FoldingAgent; 8 | 9 | impl Agent for FoldingAgent { 10 | fn act(self: &mut FoldingAgent, _id: u128, game_state: &GameState) -> AgentAction { 11 | let count = game_state.current_round_num_active_players() + game_state.num_all_in_players(); 12 | if count == 1 { 13 | AgentAction::Bet(game_state.current_round_bet()) 14 | } else { 15 | AgentAction::Fold 16 | } 17 | } 18 | } 19 | 20 | /// Default Generator for `FoldingAgent`. 21 | #[derive(Debug, Clone, Copy, Default)] 22 | pub struct FoldingAgentGenerator; 23 | 24 | impl AgentGenerator for FoldingAgentGenerator { 25 | fn generate(&self, _game_state: &GameState) -> Box { 26 | Box::new(FoldingAgent) 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use approx::assert_relative_eq; 33 | use rand::{SeedableRng, rngs::StdRng}; 34 | 35 | use crate::arena::{HoldemSimulationBuilder, game_state::Round}; 36 | 37 | use super::*; 38 | 39 | #[test_log::test] 40 | fn test_folding_agents() { 41 | let stacks = vec![100.0; 2]; 42 | let mut rng = StdRng::seed_from_u64(420); 43 | 44 | let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 45 | let mut sim = HoldemSimulationBuilder::default() 46 | .game_state(game_state) 47 | .agents(vec![Box::new(FoldingAgent {}), Box::new(FoldingAgent {})]) 48 | .build() 49 | .unwrap(); 50 | 51 | sim.run(&mut rng); 52 | 53 | assert_eq!(sim.game_state.num_active_players(), 1); 54 | assert_eq!(sim.game_state.round, Round::Complete); 55 | 56 | assert_relative_eq!(15.0_f32, sim.game_state.player_bet.iter().sum()); 57 | 58 | assert_relative_eq!(15.0_f32, sim.game_state.player_winnings.iter().sum()); 59 | assert_relative_eq!(15.0_f32, sim.game_state.player_winnings[1]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/arena/historian/directory_historian.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs::File, path::PathBuf}; 2 | 3 | use crate::arena::action::Action; 4 | 5 | use super::Historian; 6 | 7 | /// A historian implementation that records game actions in a directory. 8 | #[derive(Debug, Clone)] 9 | pub struct DirectoryHistorian { 10 | base_path: PathBuf, 11 | sequence: HashMap>, 12 | } 13 | 14 | impl DirectoryHistorian { 15 | /// Creates a new `DirectoryHistorian` with the specified base path. 16 | /// 17 | /// # Arguments 18 | /// 19 | /// * `base_path` - The base path where the game action files will be 20 | /// stored. 21 | pub fn new(base_path: PathBuf) -> Self { 22 | DirectoryHistorian { 23 | base_path, 24 | sequence: HashMap::new(), 25 | } 26 | } 27 | } 28 | impl Historian for DirectoryHistorian { 29 | /// Records all the game actions into a file in the specified directory. 30 | /// 31 | /// # Arguments 32 | /// 33 | /// * `id` - The ID of the game. 34 | /// * `_game_state` - The current game state. 35 | /// * `action` - The action to record. 36 | /// 37 | /// # Errors 38 | /// 39 | /// Returns an error if there was a problem recording the action. 40 | fn record_action( 41 | &mut self, 42 | id: u128, 43 | _game_state: &crate::arena::GameState, 44 | action: crate::arena::action::Action, 45 | ) -> Result<(), super::HistorianError> { 46 | // First make sure the base_path exists at all 47 | if !self.base_path.exists() { 48 | std::fs::create_dir_all(&self.base_path)?; 49 | } 50 | 51 | let game_path = self.base_path.join(id.to_string()).with_extension("json"); 52 | // Create and write the whole sequence to the file every time just in case 53 | // something fails. 54 | let file = File::create(game_path)?; 55 | // Add the new action to the sequence 56 | let sequence = self.sequence.entry(id).or_default(); 57 | sequence.push(action); 58 | 59 | // Write the sequence to the file 60 | Ok(serde_json::to_writer_pretty(&file, sequence)?) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rs_poker" 3 | version = "4.1.0" 4 | authors = ["Elliott Clark "] 5 | keywords = ["cards", "poker"] 6 | categories = ["games"] 7 | homepage = "https://docs.rs/rs_poker" 8 | repository = "https://github.com/elliottneilclark/rs-poker" 9 | description = "A library to help with any Rust code dealing with poker. This includes card values, suits, hands, hand ranks, 5 card hand strength calculation, 7 card hand strength calulcation, and monte carlo game simulation helpers." 10 | readme = "README.md" 11 | license = "Apache-2.0" 12 | edition = "2024" 13 | 14 | [dependencies] 15 | rand = "~0.9.1" 16 | thiserror = "~2.0.12" 17 | serde = { version = "1.0.219", optional = true, features = ["derive"] } 18 | serde_json = { version = "~1.0.140", optional = true } 19 | arbitrary = { version = "~1.4.1", optional = true, features = ["derive"] } 20 | tracing = { version = "~0.1.41", optional = true } 21 | approx = { version = "~0.5.1", optional = true } 22 | little-sorry = { version = "~1.1.0", optional = true, features = [] } 23 | ndarray = { version = "~0.16.1", optional = true } 24 | chrono = { version = "~0.4.41", optional = true, features = ["serde"] } 25 | 26 | [dev-dependencies] 27 | criterion = "~0.6.0" 28 | test-log = { version = "~0.2.17", features = ["trace", "log"] } 29 | tracing-subscriber = { version = "~0.3.19", default-features = true, features = [ 30 | "env-filter", 31 | "fmt", 32 | ] } 33 | env_logger = { version = "~0.11.8" } 34 | approx = { version = "~0.5.1" } 35 | tempfile = "~3.20.0" 36 | 37 | [target.'cfg(not(target_env = "msvc"))'.dev-dependencies] 38 | tikv-jemallocator = { version = "~0.6.0", features = [ 39 | "profiling", 40 | "unprefixed_malloc_on_supported_platforms", 41 | ] } 42 | 43 | [features] 44 | default = ["arena", "serde"] 45 | serde = ["dep:serde", "dep:serde_json"] 46 | arena = ["dep:tracing", "dep:little-sorry", "dep:ndarray"] 47 | arena-test-util = ["arena", "dep:approx"] 48 | open-hand-history = ["serde", "dep:chrono"] 49 | 50 | [[bench]] 51 | name = "arena" 52 | harness = false 53 | required-features = ["arena"] 54 | 55 | [[bench]] 56 | name = "monte_carlo_game" 57 | harness = false 58 | 59 | [[bench]] 60 | name = "holdem_starting_hand" 61 | harness = false 62 | 63 | [[bench]] 64 | name = "iter" 65 | harness = false 66 | 67 | [[bench]] 68 | name = "parse" 69 | harness = false 70 | 71 | [[bench]] 72 | name = "rank" 73 | harness = false 74 | 75 | [[bench]] 76 | name = "icm_sim" 77 | harness = false 78 | 79 | [[bench]] 80 | name = "deal_deck" 81 | harness = false 82 | 83 | [profile.release] 84 | debug = true 85 | lto = true 86 | -------------------------------------------------------------------------------- /src/arena/agent/mod.rs: -------------------------------------------------------------------------------- 1 | //! `Agent`s are the automatic playes in the poker simulations. They are the 2 | //! logic and strategies behind figuring out expected value. 3 | //! 4 | //! Some basic agents are provided as a way of testing baseline value. 5 | mod all_in; 6 | mod calling; 7 | mod folding; 8 | mod random; 9 | mod replay; 10 | 11 | use super::{Historian, action::AgentAction, game_state::GameState}; 12 | /// This is the trait that you need to implement in order to implenet 13 | /// different strategies. It's up to you to to implement the logic and state. 14 | /// 15 | /// Agents must implment Clone. This punts all mutex or reference counting 16 | /// issues to the writer of agent but also allows single threaded simulations 17 | /// not to need `Arc>`'s overhead. 18 | pub trait Agent { 19 | /// This is the method that will be called by the game to get the action 20 | fn act(&mut self, id: u128, game_state: &GameState) -> AgentAction; 21 | 22 | // Some Agents may need to be able to see the changes in the game 23 | // state. This is the method that will be called to create historians 24 | // when starting a new simulation game. 25 | fn historian(&self) -> Option> { 26 | None 27 | } 28 | } 29 | 30 | /// AgentBuilder is a trait that is used to build agents for tournaments 31 | /// where each simulation needs a new agent. 32 | pub trait AgentGenerator { 33 | /// This method is called before each game to build a new agent. 34 | fn generate(&self, game_state: &GameState) -> Box; 35 | } 36 | 37 | pub trait CloneAgent: Agent { 38 | fn clone_box(&self) -> Box; 39 | } 40 | 41 | impl CloneAgent for T 42 | where 43 | T: 'static + Agent + Clone, 44 | { 45 | fn clone_box(&self) -> Box { 46 | Box::new(self.clone()) 47 | } 48 | } 49 | 50 | pub struct CloneAgentGenerator { 51 | agent: T, 52 | } 53 | 54 | impl CloneAgentGenerator 55 | where 56 | T: CloneAgent, 57 | { 58 | pub fn new(agent: T) -> Self { 59 | CloneAgentGenerator { agent } 60 | } 61 | } 62 | 63 | impl AgentGenerator for CloneAgentGenerator 64 | where 65 | T: CloneAgent, 66 | { 67 | fn generate(&self, _game_state: &GameState) -> Box { 68 | self.agent.clone_box() 69 | } 70 | } 71 | 72 | pub use all_in::{AllInAgent, AllInAgentGenerator}; 73 | pub use calling::{CallingAgent, CallingAgentGenerator}; 74 | pub use folding::{FoldingAgent, FoldingAgentGenerator}; 75 | pub use random::{RandomAgent, RandomAgentGenerator, RandomPotControlAgent}; 76 | pub use replay::{SliceReplayAgent, VecReplayAgent}; 77 | -------------------------------------------------------------------------------- /src/open_hand_history/writer.rs: -------------------------------------------------------------------------------- 1 | use std::fs::OpenOptions; 2 | use std::io::{self, Write}; 3 | use std::path::Path; 4 | 5 | use super::hand_history::{HandHistory, OpenHandHistoryWrapper}; 6 | 7 | /// Appends a hand history to a file in JSON Lines format 8 | pub fn append_hand(path: &Path, hand: HandHistory) -> io::Result<()> { 9 | // Create file if it doesn't exist, append if it does 10 | let mut file = OpenOptions::new().create(true).append(true).open(path)?; 11 | 12 | // Wrap the hand history in the OHH wrapper 13 | let wrapped = OpenHandHistoryWrapper { ohh: hand }; 14 | 15 | // Serialize to JSON and append newline 16 | serde_json::to_writer(&mut file, &wrapped)?; 17 | writeln!(file)?; // Newline at the end 18 | writeln!(file)?; // Extra newline for separation 19 | Ok(()) 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use super::*; 25 | use std::fs; 26 | use tempfile::NamedTempFile; 27 | 28 | #[test] 29 | fn test_append_hand() { 30 | // Create temp file that gets cleaned up after test 31 | let temp_file = NamedTempFile::new().unwrap(); 32 | let path = temp_file.path(); 33 | 34 | // Create minimal hand history for testing 35 | let hand = HandHistory { 36 | spec_version: "1.4.7".to_string(), 37 | site_name: "Test Site".to_string(), 38 | network_name: "Test Network".to_string(), 39 | internal_version: "1.0".to_string(), 40 | tournament: false, 41 | tournament_info: None, 42 | game_number: "123".to_string(), 43 | start_date_utc: None, 44 | table_name: "Test Table".to_string(), 45 | table_handle: None, 46 | table_skin: None, 47 | game_type: super::super::hand_history::GameType::Holdem, 48 | bet_limit: None, 49 | table_size: 9, 50 | currency: "USD".to_string(), 51 | dealer_seat: 1, 52 | small_blind_amount: 1.0, 53 | big_blind_amount: 2.0, 54 | ante_amount: 0.0, 55 | hero_player_id: None, 56 | players: vec![], 57 | rounds: vec![], 58 | pots: vec![], 59 | tournament_bounties: None, 60 | }; 61 | 62 | // Write hand to file 63 | append_hand(path, hand).unwrap(); 64 | 65 | // Verify file contents 66 | let contents = fs::read_to_string(path).unwrap(); 67 | dbg!(&contents); 68 | assert!(contents.contains("Test Site")); 69 | assert!(contents.ends_with("\n\n")); // Check for double newline at end 70 | 71 | // Parse contents back to verify format 72 | let parsed: OpenHandHistoryWrapper = serde_json::from_str(contents.trim()).unwrap(); 73 | assert_eq!(parsed.ohh.site_name, "Test Site"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rs-poker 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/rs-poker.svg)](https://crates.io/crates/rs-poker) 4 | [![Docs.rs](https://docs.rs/rs_poker/badge.svg)](https://docs.rs/rs_poker) 5 | 6 | RS Poker is a rust library aimed to be a good starting place for many poker rust 7 | codes. Correctness and performance are the two primary goals. 8 | 9 | ## Core 10 | 11 | The Core module contains code not specific to different types of poker games. It 12 | contains: 13 | 14 | - Suit type 15 | - Value type 16 | - Card type 17 | - Deck 18 | - Hand iteration 19 | - Poker hand rank type 20 | - Poker hand evaluation for five-card hands. 21 | - Poker hand evaluation for seven card hands. 22 | - PlayerBitSet is suitable for keeping track of boolean values on a table. 23 | 24 | The poker hand (5 cards) evaluation will rank a hand in ~20 nanoseconds per 25 | hand. That means that 50 Million hands per second can be ranked per CPU core. 26 | The seven-card hand evaluation will rank a hand in < 25 ns. 27 | 28 | The hand evaluation is accurate. `rs-poker` does not rely on just a single 29 | kicker. This accuracy allows for breaking ties on hands that are closer. 30 | 31 | ## Holdem 32 | 33 | The holdem module contains code that is specific to holdem. It currently 34 | contains: 35 | 36 | - Starting hand enumeration 37 | - Hand range parsing 38 | - Monte Carlo game simulation helpers. 39 | 40 | ## Arena 41 | 42 | Arena is a feature that allows the creating of agents that play a simulated 43 | Texas Holdem poker game. These autonomous agent vs agent games are ideal for 44 | determining the strength of automated strategies. Additionally, agent vs agent 45 | arenas are a good way of quickly playing lots of GTO poker. 46 | 47 | Do you think you can create a better poker agent? The Arena module is a good place to 48 | start. The Arena module contains: 49 | 50 | - Holdem simulation struct for the overall status of the simulation 51 | - Game state for the state of the current game 52 | - Agent trait that you can implement to create your more potent poker agent. 53 | - A few example Agents. 54 | - Historians who can watch every action in a simulation as it happens 55 | 56 | ### Arena CFR Agent 57 | 58 | `CFRAgent` is an agent that uses the Counterfactual Regret Minimization 59 | algorithm to choose the best action. The agent is a good starting point for 60 | creating a strong poker agent. 61 | 62 | To implement your own strategy you will need to build a new `ActionGenerator`. 63 | The `ActionGenerator` is responsible for generating all possible actions for a 64 | given game state. The `CFRAgent` will then explore possible results of trying 65 | the actions suggested by `ActionGenerator`. The Agent will choose the action it 66 | would most regret not taking. 67 | 68 | ## Testing 69 | 70 | The code is well-tested and benchmarked. If you find something that looks like a 71 | bug, please submit a PR with an updated test code. 72 | 73 | 5 Card + Hand iteration is used with fuzzing to validate the seven-card hand 74 | evaluation. 75 | 76 | Fuzzing is used to validate game simulation via replay generation. 77 | 78 | Multi-agent simulations are used to validate correctness and performance. 79 | -------------------------------------------------------------------------------- /src/arena/competition/sim_iterator.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{ 2 | AgentGenerator, GameState, HoldemSimulation, HoldemSimulationBuilder, 3 | historian::HistorianGenerator, 4 | }; 5 | 6 | pub struct StandardSimulationIterator 7 | where 8 | G: Iterator, 9 | { 10 | agent_generators: Vec>, 11 | historian_generators: Vec>, 12 | game_state_iterator: G, 13 | } 14 | 15 | impl StandardSimulationIterator 16 | where 17 | G: Iterator, 18 | { 19 | pub fn new( 20 | agent_generators: Vec>, 21 | historian_generators: Vec>, 22 | game_state_iterator: G, 23 | ) -> StandardSimulationIterator { 24 | StandardSimulationIterator { 25 | agent_generators, 26 | historian_generators, 27 | game_state_iterator, 28 | } 29 | } 30 | } 31 | 32 | impl StandardSimulationIterator 33 | where 34 | G: Iterator, 35 | { 36 | fn generate(&mut self, game_state: GameState) -> Option { 37 | let agents = self 38 | .agent_generators 39 | .iter() 40 | .map(|g| g.generate(&game_state)) 41 | .collect(); 42 | let historians = self 43 | .historian_generators 44 | .iter() 45 | .map(|g| g.generate(&game_state)) 46 | .collect(); 47 | 48 | HoldemSimulationBuilder::default() 49 | .agents(agents) 50 | .historians(historians) 51 | .game_state(game_state) 52 | .build() 53 | .ok() 54 | } 55 | } 56 | 57 | impl Iterator for StandardSimulationIterator 58 | where 59 | G: Iterator, 60 | { 61 | type Item = HoldemSimulation; 62 | 63 | fn next(&mut self) -> Option { 64 | if let Some(game_state) = self.game_state_iterator.next() { 65 | self.generate(game_state) 66 | } else { 67 | None 68 | } 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use crate::arena::{ 75 | GameState, agent::FoldingAgentGenerator, game_state::CloneGameStateGenerator, 76 | }; 77 | 78 | use super::*; 79 | 80 | #[test] 81 | fn test_static_simulation_generator() { 82 | let generators: Vec> = vec![ 83 | Box::::default(), 84 | Box::::default(), 85 | Box::::default(), 86 | ]; 87 | let stacks = vec![100.0; 3]; 88 | let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 89 | let mut sim_gen = StandardSimulationIterator::new( 90 | generators, 91 | vec![], 92 | CloneGameStateGenerator::new(game_state), 93 | ); 94 | 95 | let _first = sim_gen 96 | .next() 97 | .expect("There should always be a first simulation"); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/arena/historian/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{GameState, action::Action}; 2 | use thiserror::Error; 3 | 4 | /// HistorianError is the error type for historian implementations. 5 | #[derive(Error, Debug)] 6 | pub enum HistorianError { 7 | #[error("Unable to record action")] 8 | UnableToRecordAction, 9 | #[error("IO Error: {0}")] 10 | IOError(#[from] std::io::Error), 11 | #[error("Borrow Mut Error: {0}")] 12 | BorrowMutError(#[from] std::cell::BorrowMutError), 13 | #[error("Borrow Error: {0}")] 14 | BorrowError(#[from] std::cell::BorrowError), 15 | #[cfg(any(test, feature = "serde"))] 16 | #[error("JSON Error: {0}")] 17 | JSONError(#[from] serde_json::Error), 18 | #[error("Unexpected CFR Node: {0}")] 19 | CFRUnexpectedNode(String), 20 | #[error("Expected Node not found in tree")] 21 | CFRNodeNotFound, 22 | } 23 | 24 | /// Historians are a way for the simulation to record or notify of 25 | /// actions while the game is progressing. This is useful for 26 | /// logging, debugging, or even for implementing a replay system. 27 | /// However it's also useful for CFR+ as each action 28 | /// moves the game along the nodes. 29 | pub trait Historian { 30 | /// This method is called by the simulation when an action is received. 31 | /// 32 | /// # Arguments 33 | /// - `id` - The id of the simulation that the action was received on. 34 | /// - `game_state` - The game state after the action was played 35 | /// - `action` - The action that was played 36 | /// 37 | /// # Returns 38 | /// - `Ok(())` if the action was recorded successfully 39 | /// - `Err(HistorianError)` if there was an error recording the action. 40 | /// 41 | /// Returning an error will cause the historian to be dropped from the 42 | /// `Simulation`. 43 | fn record_action( 44 | &mut self, 45 | id: u128, 46 | game_state: &GameState, 47 | action: Action, 48 | ) -> Result<(), HistorianError>; 49 | } 50 | 51 | /// `HistorianGenerator` is a trait that is used to build historians 52 | /// for tournaments where each simulation needs a new historian. 53 | pub trait HistorianGenerator { 54 | /// This method is called before each game to build a new historian. 55 | fn generate(&self, game_state: &GameState) -> Box; 56 | } 57 | 58 | pub trait CloneHistorian: Historian { 59 | fn clone_box(&self) -> Box; 60 | } 61 | 62 | impl CloneHistorian for T 63 | where 64 | T: 'static + Historian + Clone, 65 | { 66 | fn clone_box(&self) -> Box { 67 | Box::new(self.clone()) 68 | } 69 | } 70 | 71 | pub struct CloneHistorianGenerator { 72 | historian: T, 73 | } 74 | 75 | impl CloneHistorianGenerator 76 | where 77 | T: CloneHistorian, 78 | { 79 | pub fn new(historian: T) -> Self { 80 | CloneHistorianGenerator { historian } 81 | } 82 | } 83 | 84 | impl HistorianGenerator for CloneHistorianGenerator 85 | where 86 | T: CloneHistorian, 87 | { 88 | fn generate(&self, _game_state: &GameState) -> Box { 89 | self.historian.clone_box() 90 | } 91 | } 92 | 93 | mod failing; 94 | mod fn_historian; 95 | mod null; 96 | mod stats_tracking; 97 | mod vec; 98 | 99 | #[cfg(any(test, feature = "serde"))] 100 | mod directory_historian; 101 | 102 | pub use failing::FailingHistorian; 103 | pub use fn_historian::FnHistorian; 104 | pub use null::NullHistorian; 105 | pub use vec::HistoryRecord; 106 | pub use vec::VecHistorian; 107 | 108 | #[cfg(any(test, feature = "serde"))] 109 | pub use directory_historian::DirectoryHistorian; 110 | 111 | pub use stats_tracking::StatsTrackingHistorian; 112 | -------------------------------------------------------------------------------- /src/arena/historian/fn_historian.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{GameState, action::Action}; 2 | 3 | use super::{Historian, HistorianError}; 4 | 5 | /// This `Agent` is an implmentation that returns 6 | /// random actions. However, it also takes in a function 7 | /// that is called when an action is received. This is 8 | /// useful for testing and debugging. 9 | #[derive(Debug, Clone)] 10 | pub struct FnHistorian { 11 | func: F, 12 | } 13 | 14 | impl Result<(), HistorianError>> FnHistorian { 15 | /// Create a new `FnHistorian` with the provided function 16 | /// that will be called when an action is received on a simulation. 17 | pub fn new(f: F) -> Self { 18 | Self { func: f } 19 | } 20 | } 21 | 22 | impl Result<(), HistorianError>> Historian 23 | for FnHistorian 24 | { 25 | fn record_action( 26 | &mut self, 27 | id: u128, 28 | game_state: &GameState, 29 | action: Action, 30 | ) -> Result<(), HistorianError> { 31 | // Call the function with the action that was received 32 | (self.func)(id, game_state, action) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use std::{cell::RefCell, rc::Rc}; 39 | 40 | use crate::arena::{Agent, HoldemSimulationBuilder, agent::RandomAgent, game_state::Round}; 41 | 42 | use super::*; 43 | 44 | #[test] 45 | fn test_can_record_actions_with_agents() { 46 | let last_action: Rc>> = Rc::new(RefCell::new(None)); 47 | let count = Rc::new(RefCell::new(0)); 48 | 49 | let agents: Vec> = (0..2) 50 | .map(|_| Box::::default() as Box) 51 | .collect(); 52 | let game_state = GameState::new_starting(vec![100.0, 100.0], 10.0, 5.0, 0.0, 0); 53 | 54 | let borrow_count = count.clone(); 55 | let borrow_last_action = last_action.clone(); 56 | 57 | let historian = Box::new(FnHistorian::new(move |_id, _game_state, action| { 58 | *borrow_count.borrow_mut() += 1; 59 | *borrow_last_action.borrow_mut() = Some(action); 60 | Ok(()) 61 | })); 62 | 63 | let mut rng = rand::rng(); 64 | 65 | let mut sim = HoldemSimulationBuilder::default() 66 | .agents(agents) 67 | .game_state(game_state) 68 | .historians(vec![historian]) 69 | .build() 70 | .unwrap(); 71 | 72 | sim.run(&mut rng); 73 | 74 | assert_ne!(0, count.take()); 75 | 76 | let act = last_action.take(); 77 | 78 | assert!(act.is_some()); 79 | 80 | assert_eq!(Some(Action::RoundAdvance(Round::Complete)), act); 81 | } 82 | 83 | #[test] 84 | fn test_fn_historian_can_withstand_error() { 85 | // A test that adds a historian that always returns an error 86 | // This shows that the historian will be dropped from the simulation 87 | // if it returns an error but the simulation will continue to run. 88 | 89 | let agents: Vec> = (0..2) 90 | .map(|_| Box::::default() as Box) 91 | .collect(); 92 | 93 | let game_state = GameState::new_starting(vec![100.0, 100.0], 10.0, 5.0, 0.0, 0); 94 | let historian = Box::new(FnHistorian::new(|_, _, _| { 95 | Err(HistorianError::UnableToRecordAction) 96 | })); 97 | 98 | let mut rng = rand::rng(); 99 | 100 | HoldemSimulationBuilder::default() 101 | .agents(agents) 102 | .game_state(game_state) 103 | .historians(vec![historian]) 104 | .panic_on_historian_error(false) 105 | .build() 106 | .unwrap() 107 | .run(&mut rng); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /benches/arena.rs: -------------------------------------------------------------------------------- 1 | use criterion::BenchmarkId; 2 | use criterion::Criterion; 3 | 4 | use criterion::criterion_group; 5 | use criterion::criterion_main; 6 | use rand::rng; 7 | use rs_poker::arena::Agent; 8 | use rs_poker::arena::GameState; 9 | use rs_poker::arena::HoldemSimulationBuilder; 10 | use rs_poker::arena::agent::RandomAgent; 11 | use rs_poker::arena::agent::RandomPotControlAgent; 12 | 13 | const STARTING_STACK: f32 = 100_000.0; 14 | const ANTE: f32 = 50.0; 15 | const SMALL_BLIND: f32 = 250.0; 16 | const BIG_BLIND: f32 = 500.0; 17 | 18 | const DEFAULT_FOLD: f64 = 0.15; 19 | const DEFAULT_CALL: f64 = 0.5; 20 | 21 | const RANDOM_CHANCES: [(f64, f64); 5] = 22 | [(0.0, 0.5), (0.15, 0.5), (0.5, 0.4), (0.0, 1.0), (0.0, 0.1)]; 23 | 24 | fn run_one_arena(num_players: usize, percent_fold: f64, percent_call: f64) -> GameState { 25 | let stacks = vec![STARTING_STACK; num_players]; 26 | let game_state = GameState::new_starting(stacks, BIG_BLIND, SMALL_BLIND, ANTE, 0); 27 | let agents: Vec> = (0..num_players) 28 | .map(|_| -> Box { 29 | Box::new(RandomAgent::new(vec![percent_fold], vec![percent_call])) 30 | }) 31 | .collect(); 32 | let mut sim = HoldemSimulationBuilder::default() 33 | .game_state(game_state) 34 | .agents(agents) 35 | .build() 36 | .unwrap(); 37 | 38 | let mut rand = rng(); 39 | 40 | sim.run(&mut rand); 41 | sim.game_state 42 | } 43 | 44 | fn run_one_pot_control_arena(num_players: usize) -> GameState { 45 | let stacks = vec![STARTING_STACK; num_players]; 46 | let game_state = GameState::new_starting(stacks, BIG_BLIND, SMALL_BLIND, ANTE, 0); 47 | let agents: Vec> = (0..num_players) 48 | .map(|_idx| -> Box { Box::new(RandomPotControlAgent::new(vec![0.3])) }) 49 | .collect(); 50 | 51 | let mut sim = HoldemSimulationBuilder::default() 52 | .game_state(game_state) 53 | .agents(agents) 54 | .build() 55 | .unwrap(); 56 | 57 | let mut rand = rng(); 58 | 59 | sim.run(&mut rand); 60 | sim.game_state 61 | } 62 | 63 | fn bench_num_random_agent_players(c: &mut Criterion) { 64 | let mut group = c.benchmark_group("arena_random_agents"); 65 | for num_players in 2..9 { 66 | group.bench_with_input( 67 | BenchmarkId::from_parameter(num_players), 68 | &num_players, 69 | |b, num_players| { 70 | b.iter(|| run_one_arena(*num_players, DEFAULT_FOLD, DEFAULT_CALL)); 71 | }, 72 | ); 73 | } 74 | 75 | group.finish(); 76 | } 77 | 78 | fn bench_random_chances_agents(c: &mut Criterion) { 79 | let mut group = c.benchmark_group("arena_random_agents"); 80 | for input in RANDOM_CHANCES { 81 | let (percent_fold, percent_call) = input; 82 | let id = format!("percent_fold: {percent_fold} percent_call: {percent_call}"); 83 | group.bench_with_input( 84 | BenchmarkId::new("arena_random_agent_choices", id), 85 | &input, 86 | |b, input| { 87 | let (percent_fold, percent_call) = input; 88 | b.iter(|| run_one_arena(6, *percent_fold, *percent_call)); 89 | }, 90 | ); 91 | } 92 | 93 | group.finish(); 94 | } 95 | 96 | fn bench_pot_control_agents(c: &mut Criterion) { 97 | let mut group = c.benchmark_group("pot_control_agents"); 98 | 99 | for num_players in 2..9 { 100 | group.bench_with_input( 101 | BenchmarkId::from_parameter(num_players), 102 | &num_players, 103 | |b, num_players| { 104 | b.iter(|| run_one_pot_control_arena(*num_players)); 105 | }, 106 | ); 107 | } 108 | 109 | group.finish(); 110 | } 111 | 112 | criterion_group!( 113 | benches, 114 | bench_num_random_agent_players, 115 | bench_pot_control_agents, 116 | bench_random_chances_agents 117 | ); 118 | criterion_main!(benches); 119 | -------------------------------------------------------------------------------- /examples/cfr_hand_demo.rs: -------------------------------------------------------------------------------- 1 | use rs_poker::arena::{ 2 | Agent, Historian, HoldemSimulationBuilder, 3 | cfr::{ 4 | BasicCFRActionGenerator, CFRAgent, ExportFormat, PerRoundFixedGameStateIteratorGen, 5 | StateStore, export_cfr_state, 6 | }, 7 | historian::DirectoryHistorian, 8 | }; 9 | 10 | fn run_simulation(num_agents: usize, export_path: Option) { 11 | // Create a game state with the specified number of agents 12 | let stacks = vec![500.0; num_agents]; 13 | let game_state = 14 | rs_poker::arena::game_state::GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 15 | 16 | let mut state_store = StateStore::new(); 17 | 18 | let states = (0..num_agents) 19 | .map(|player_idx| state_store.new_state(game_state.clone(), player_idx)) 20 | .collect::>(); 21 | 22 | let agents: Vec<_> = states 23 | .iter() 24 | .map(|(cfr_state, traversal_state)| { 25 | Box::new( 26 | // Create a CFR Agent for each player 27 | // They have their own CFR state and 28 | // and for now a fixed game state iterator 29 | // that will try a very few hands 30 | CFRAgent::::new( 31 | state_store.clone(), 32 | cfr_state.clone(), 33 | traversal_state.clone(), 34 | // please note that this is way too small 35 | // for a real CFR simulation, but it is 36 | // enough to demonstrate the CFR state tree 37 | // and the export of the game history 38 | PerRoundFixedGameStateIteratorGen::default(), 39 | ), 40 | ) 41 | }) 42 | .collect(); 43 | 44 | let mut historians: Vec> = Vec::new(); 45 | 46 | if let Some(path) = export_path.clone() { 47 | // If a path is provided, we create a directory historian 48 | // to store the game history 49 | let dir_hist = DirectoryHistorian::new(path); 50 | 51 | // We don't need to create the dir_hist because the 52 | // DirectoryHistorian already does that on first action to record 53 | historians.push(Box::new(dir_hist)); 54 | } 55 | 56 | let dyn_agents = agents.into_iter().map(|a| a as Box).collect(); 57 | 58 | let mut sim = HoldemSimulationBuilder::default() 59 | .game_state(game_state) 60 | .agents(dyn_agents) 61 | .historians(historians) 62 | .build() 63 | .unwrap(); 64 | 65 | let mut rand = rand::rng(); 66 | sim.run(&mut rand); 67 | 68 | // If there's an export path then we want to export each of the states 69 | if let Some(path) = export_path.clone() { 70 | for (i, (cfr_state, _)) in states.iter().enumerate() { 71 | // Export the CFR state to JSON 72 | export_cfr_state( 73 | cfr_state, 74 | path.join(format!("cfr_state_{i}.svg")).as_path(), 75 | ExportFormat::Svg, 76 | ) 77 | .expect("failed to export cfr state"); 78 | } 79 | } 80 | } 81 | 82 | // Since simulation runs hot and heavy anything we can do to reduce the 83 | // Allocation overhead is a good thing. 84 | // 85 | #[cfg(not(target_env = "msvc"))] 86 | use tikv_jemallocator::Jemalloc; 87 | 88 | #[cfg(not(target_env = "msvc"))] 89 | #[global_allocator] 90 | static GLOBAL: Jemalloc = Jemalloc; 91 | 92 | fn main() { 93 | // The first argument is the number of agents 94 | let num_agents = std::env::args() 95 | .nth(1) 96 | .expect("number of agents") 97 | .parse::() 98 | .expect("invalid number of agents"); 99 | 100 | // The second argument is an optional path to where we should store 101 | // The JSON game history and the CFR state tree diagram 102 | // If no path is provided, no files will be created 103 | let export_path = std::env::args().nth(2).map(std::path::PathBuf::from); 104 | 105 | run_simulation(num_agents, export_path.clone()); 106 | } 107 | -------------------------------------------------------------------------------- /src/open_hand_history/serde_utils.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{self, Deserialize, Deserializer, Serializer}; 3 | 4 | pub mod iso8601 { 5 | use super::*; 6 | 7 | pub fn serialize(date: &Option>, serializer: S) -> Result 8 | where 9 | S: Serializer, 10 | { 11 | match date { 12 | Some(date) => serializer.serialize_str(&date.to_rfc3339()), 13 | None => serializer.serialize_none(), 14 | } 15 | } 16 | 17 | pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> 18 | where 19 | D: Deserializer<'de>, 20 | { 21 | let s: Option = Option::deserialize(deserializer)?; 22 | match s { 23 | Some(s) => { 24 | // Try parsing as RFC3339 first 25 | DateTime::parse_from_rfc3339(&s) 26 | .or_else(|_| DateTime::parse_from_rfc3339(&format!("{s}Z"))) 27 | .map(|dt| Some(dt.with_timezone(&Utc))) 28 | .map_err(serde::de::Error::custom) 29 | } 30 | None => Ok(None), 31 | } 32 | } 33 | } 34 | 35 | pub fn empty_string_is_none<'de, D, T>(deserializer: D) -> Result, D::Error> 36 | where 37 | D: Deserializer<'de>, 38 | T: serde::de::DeserializeOwned, 39 | { 40 | #[derive(Deserialize, Debug)] 41 | #[serde(untagged)] 42 | enum Wrapper { 43 | String(String), 44 | Result(T), 45 | } 46 | 47 | match Wrapper::deserialize(deserializer)? { 48 | Wrapper::String(s) if s.is_empty() => Ok(None), 49 | Wrapper::String(_) => Err(serde::de::Error::custom("expected empty string or vector")), 50 | Wrapper::Result(v) => Ok(Some(v)), 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::*; 57 | use serde::{Deserialize, Serialize}; 58 | 59 | #[derive(Debug, Serialize, Deserialize)] 60 | struct TestDate { 61 | #[serde(with = "iso8601")] 62 | date: Option>, 63 | } 64 | 65 | #[test] 66 | fn test_valid_dates() { 67 | let json = r#"{"date": "2023-11-14T12:34:56Z"}"#; 68 | let parsed: TestDate = serde_json::from_str(json).unwrap(); 69 | assert!(parsed.date.is_some()); 70 | 71 | let json = r#"{"date": "2023-11-14T12:34:56+00:00"}"#; 72 | let parsed: TestDate = serde_json::from_str(json).unwrap(); 73 | assert!(parsed.date.is_some()); 74 | 75 | let json = r#"{"date": "2023-11-14T12:34:56.123Z"}"#; 76 | let parsed: TestDate = serde_json::from_str(json).unwrap(); 77 | assert!(parsed.date.is_some()); 78 | } 79 | 80 | #[test] 81 | fn test_from_hand_history_example() { 82 | let json = r#"{"date": "2017-12-31T09:45:26Z"}"#; 83 | let parsed: TestDate = serde_json::from_str(json).unwrap(); 84 | assert_eq!( 85 | parsed.date.unwrap().to_rfc3339(), 86 | "2017-12-31T09:45:26+00:00" 87 | ); 88 | } 89 | 90 | #[test] 91 | fn test_another_from_hh_example() { 92 | let json = r#"{"date": "2020-04-07T14:32:50"}"#; 93 | let parsed: TestDate = serde_json::from_str(json).unwrap(); 94 | assert_eq!( 95 | parsed.date.unwrap().to_rfc3339(), 96 | "2020-04-07T14:32:50+00:00" 97 | ); 98 | } 99 | 100 | #[test] 101 | fn test_invalid_dates() { 102 | // Invalid format 103 | let json = r#"{"date": "2023-11-14"}"#; 104 | assert!(serde_json::from_str::(json).is_err()); 105 | 106 | // Invalid date 107 | let json = r#"{"date": "2023-13-14T12:34:56Z"}"#; 108 | assert!(serde_json::from_str::(json).is_err()); 109 | 110 | // Invalid time 111 | let json = r#"{"date": "2023-11-14T25:34:56Z"}"#; 112 | assert!(serde_json::from_str::(json).is_err()); 113 | } 114 | 115 | #[test] 116 | fn test_none() { 117 | let json = r#"{"date": null}"#; 118 | let parsed: TestDate = serde_json::from_str(json).unwrap(); 119 | assert!(parsed.date.is_none()); 120 | } 121 | 122 | #[test] 123 | fn test_roundtrip() { 124 | let original = TestDate { 125 | date: Some(Utc::now()), 126 | }; 127 | 128 | let serialized = serde_json::to_string(&original).unwrap(); 129 | let deserialized: TestDate = serde_json::from_str(&serialized).unwrap(); 130 | 131 | assert_eq!( 132 | original.date.unwrap().to_rfc3339(), 133 | deserialized.date.unwrap().to_rfc3339() 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /examples/cfr_export_demo.rs: -------------------------------------------------------------------------------- 1 | use rs_poker::arena::GameState; 2 | use rs_poker::arena::cfr::{ 3 | CFRState, ExportFormat, NodeData, PlayerData, TerminalData, export_cfr_state, 4 | }; 5 | use std::path::Path; 6 | 7 | /// Creates an example CFR state tree for demonstration purposes. 8 | /// The tree represents a simple poker game with two players, including fold, 9 | /// call, and raise actions. 10 | fn create_example_cfr() -> CFRState { 11 | // Create a game state with 2 players 12 | let game_state = GameState::new_starting(vec![100.0; 2], 10.0, 5.0, 0.0, 0); 13 | let mut cfr_state = CFRState::new(game_state); 14 | 15 | // Root -> Player 0 decision 16 | let player0_node = NodeData::Player(PlayerData { 17 | regret_matcher: None, 18 | player_idx: 0, 19 | }); 20 | let player0_idx = cfr_state.add(0, 0, player0_node); 21 | 22 | // Player 0 fold 23 | let terminal_fold = NodeData::Terminal(TerminalData::new(-10.0)); 24 | let _fold_idx = cfr_state.add(player0_idx, 0, terminal_fold); 25 | 26 | // Player 0 call 27 | let player0_call = cfr_state.add(player0_idx, 1, NodeData::Chance); 28 | 29 | // Player 0 raise 30 | let player0_raise = cfr_state.add(player0_idx, 2, NodeData::Chance); 31 | 32 | // After call - chance node (dealing flop) 33 | for i in 0..3 { 34 | // Create 3 sample card possibilities 35 | let player1_node = NodeData::Player(PlayerData { 36 | regret_matcher: None, 37 | player_idx: 1, 38 | }); 39 | let player1_idx = cfr_state.add(player0_call, i, player1_node); 40 | 41 | // Player 1 fold 42 | let p1_fold_terminal = NodeData::Terminal(TerminalData::new(15.0)); 43 | cfr_state.add(player1_idx, 0, p1_fold_terminal); 44 | 45 | // Player 1 call 46 | let p1_call_terminal = NodeData::Terminal(TerminalData::new(5.0)); 47 | cfr_state.add(player1_idx, 1, p1_call_terminal); 48 | } 49 | 50 | // After raise - player 1 decision 51 | let player1_vs_raise = NodeData::Player(PlayerData { 52 | regret_matcher: None, 53 | player_idx: 1, 54 | }); 55 | let player1_vs_raise_idx = cfr_state.add(player0_raise, 0, player1_vs_raise); 56 | 57 | // Player 1 fold vs raise 58 | let p1_fold_vs_raise = NodeData::Terminal(TerminalData::new(20.0)); 59 | cfr_state.add(player1_vs_raise_idx, 0, p1_fold_vs_raise); 60 | 61 | // Player 1 call vs raise - goes to another chance node 62 | let chance_after_call_vs_raise = cfr_state.add(player1_vs_raise_idx, 1, NodeData::Chance); 63 | 64 | // Final terminal node after chance 65 | let final_terminal = NodeData::Terminal(TerminalData::new(30.0)); 66 | cfr_state.add(chance_after_call_vs_raise, 0, final_terminal); 67 | 68 | // Increment some counts to simulate traversals 69 | cfr_state.increment_count(player0_idx, 1).unwrap(); // Call was taken once 70 | cfr_state.increment_count(player0_idx, 2).unwrap(); // Raise was taken twice 71 | cfr_state.increment_count(player0_idx, 2).unwrap(); 72 | 73 | cfr_state 74 | } 75 | 76 | /// This example demonstrates the export functionality of the CFR state. 77 | /// It creates a simple CFR tree and exports it to DOT, PNG, and SVG formats. 78 | fn main() { 79 | // Create an example CFR state 80 | let cfr_state = create_example_cfr(); 81 | 82 | // Export the CFR state to various formats 83 | let export_path = Path::new("examples/exports/cfr_tree"); 84 | 85 | println!("Exporting CFR state to {}", export_path.display()); 86 | 87 | // Export to DOT format 88 | if let Err(e) = export_cfr_state(&cfr_state, export_path, ExportFormat::Dot) { 89 | eprintln!("Error exporting to DOT: {e}"); 90 | } else { 91 | println!( 92 | "Successfully exported to DOT format: {}.dot", 93 | export_path.display() 94 | ); 95 | } 96 | 97 | // Export to PNG format (requires Graphviz) 98 | if let Err(e) = export_cfr_state(&cfr_state, export_path, ExportFormat::Png) { 99 | eprintln!("Error exporting to PNG: {e}"); 100 | } else { 101 | println!( 102 | "Successfully exported to PNG format: {}.png", 103 | export_path.display() 104 | ); 105 | } 106 | 107 | // Export to SVG format (requires Graphviz) 108 | if let Err(e) = export_cfr_state(&cfr_state, export_path, ExportFormat::Svg) { 109 | eprintln!("Error exporting to SVG: {e}"); 110 | } else { 111 | println!( 112 | "Successfully exported to SVG format: {}.svg", 113 | export_path.display() 114 | ); 115 | } 116 | 117 | // Export to all formats 118 | if let Err(e) = export_cfr_state(&cfr_state, export_path, ExportFormat::All) { 119 | eprintln!("Error exporting to all formats: {e}"); 120 | } else { 121 | println!("Successfully exported to all formats"); 122 | } 123 | 124 | println!("Files have been created in the examples/exports directory"); 125 | } 126 | -------------------------------------------------------------------------------- /src/arena/action.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{Card, Hand, PlayerBitSet, Rank}; 2 | 3 | use super::game_state::Round; 4 | 5 | /// Represents an action that an agent can take in a game. 6 | #[derive(Debug, Clone, PartialEq)] 7 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 8 | #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] 9 | pub enum AgentAction { 10 | /// Folds the current hand. 11 | Fold, 12 | /// Matches the current bet. 13 | Call, 14 | /// Bets the specified amount of money. 15 | Bet(f32), 16 | /// Go all-in 17 | AllIn, 18 | } 19 | 20 | #[derive(Debug, Clone, PartialEq)] 21 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 22 | /// The game has started. 23 | pub struct GameStartPayload { 24 | pub ante: f32, 25 | pub small_blind: f32, 26 | pub big_blind: f32, 27 | } 28 | 29 | #[derive(Debug, Clone, PartialEq)] 30 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 31 | pub struct PlayerSitPayload { 32 | pub idx: usize, 33 | pub player_stack: f32, 34 | } 35 | 36 | /// Each player is dealt a card. This is the payload for the event. 37 | #[derive(Debug, Clone, PartialEq, Hash)] 38 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 39 | pub struct DealStartingHandPayload { 40 | pub card: Card, 41 | pub idx: usize, 42 | } 43 | 44 | #[derive(Debug, Clone, PartialEq)] 45 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 46 | pub enum ForcedBetType { 47 | Ante, 48 | SmallBlind, 49 | BigBlind, 50 | } 51 | 52 | /// A player tried to play an action and failed 53 | #[derive(Debug, Clone, PartialEq)] 54 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 55 | pub struct ForcedBetPayload { 56 | /// A bet that the player is forced to make 57 | /// The amount is the forced amount, not the final 58 | /// amount which could be lower if that puts the player all in. 59 | pub bet: f32, 60 | pub player_stack: f32, 61 | pub idx: usize, 62 | pub forced_bet_type: ForcedBetType, 63 | } 64 | 65 | #[derive(Debug, Clone, PartialEq)] 66 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 67 | /// A player tried to play an action and failed 68 | pub struct PlayedActionPayload { 69 | // The tried Action 70 | pub action: AgentAction, 71 | 72 | pub idx: usize, 73 | pub round: Round, 74 | pub player_stack: f32, 75 | 76 | pub starting_pot: f32, 77 | pub final_pot: f32, 78 | 79 | pub starting_bet: f32, 80 | pub final_bet: f32, 81 | 82 | pub starting_min_raise: f32, 83 | pub final_min_raise: f32, 84 | 85 | pub starting_player_bet: f32, 86 | pub final_player_bet: f32, 87 | 88 | pub players_active: PlayerBitSet, 89 | pub players_all_in: PlayerBitSet, 90 | } 91 | 92 | impl PlayedActionPayload { 93 | pub fn raise_amount(&self) -> f32 { 94 | self.final_bet - self.starting_bet 95 | } 96 | } 97 | 98 | #[derive(Debug, Clone, PartialEq)] 99 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 100 | /// A player tried to play an action and failed 101 | pub struct FailedActionPayload { 102 | // The tried Action 103 | pub action: AgentAction, 104 | // The result action 105 | pub result: PlayedActionPayload, 106 | } 107 | 108 | #[derive(Debug, Clone, PartialEq)] 109 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 110 | pub struct AwardPayload { 111 | pub total_pot: f32, 112 | pub award_amount: f32, 113 | pub rank: Option, 114 | pub hand: Option, 115 | pub idx: usize, 116 | } 117 | 118 | /// Represents an action that can happen in a game. 119 | #[derive(Debug, Clone, PartialEq)] 120 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 121 | pub enum Action { 122 | GameStart(GameStartPayload), 123 | PlayerSit(PlayerSitPayload), 124 | DealStartingHand(DealStartingHandPayload), 125 | /// The round has advanced. 126 | RoundAdvance(Round), 127 | /// A player has played an action. 128 | PlayedAction(PlayedActionPayload), 129 | /// The player tried and failed to take some action. 130 | /// If the action failed then there is no PlayedAction event coming. 131 | /// 132 | /// Players can fail to fold when there's no money being wagered. 133 | /// Players can fail to bet when they bet an illegal amount. 134 | FailedAction(FailedActionPayload), 135 | 136 | /// A player/agent was forced to make a bet. 137 | ForcedBet(ForcedBetPayload), 138 | /// A community card has been dealt. 139 | DealCommunity(Card), 140 | /// There was some pot given to a player 141 | Award(AwardPayload), 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use super::*; 147 | 148 | #[test] 149 | fn test_bet() { 150 | let a = AgentAction::Bet(100.0); 151 | assert_eq!(AgentAction::Bet(100.0), a); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/core/card_iter.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{Card, FlatDeck}; 2 | 3 | /// Given some cards create sets of possible groups of cards. 4 | #[derive(Debug)] 5 | pub struct CardIter<'a> { 6 | /// All the possible cards that can be dealt 7 | possible_cards: &'a [Card], 8 | 9 | /// Set of current offsets being used to create card sets. 10 | idx: Vec, 11 | 12 | /// size of card sets requested. 13 | num_cards: usize, 14 | } 15 | 16 | /// `CardIter` is a container for cards and current state. 17 | impl CardIter<'_> { 18 | /// Create a new `CardIter` from a slice of cards. 19 | /// `num_cards` represents how many cards should be in the resulting vector. 20 | pub fn new(possible_cards: &[Card], num_cards: usize) -> CardIter<'_> { 21 | let mut idx: Vec = (0..num_cards).collect(); 22 | if num_cards > 1 { 23 | idx[num_cards - 1] -= 1; 24 | } 25 | CardIter { 26 | possible_cards, 27 | idx, 28 | num_cards, 29 | } 30 | } 31 | } 32 | 33 | /// The actual `Iterator` for `Card`'s. 34 | impl Iterator for CardIter<'_> { 35 | type Item = Vec; 36 | fn next(&mut self) -> Option> { 37 | // This is a complete hack. 38 | // 39 | // Basically if num_cards == 1 then CardIter::new couldn't 40 | // set the last index to one less than the starting index, 41 | // because doing so would cause the unsigend usize to roll over. 42 | // That means that we need this hack here. 43 | if self.num_cards == 1 { 44 | if self.idx[0] < self.possible_cards.len() { 45 | let c = self.possible_cards[self.idx[0]]; 46 | self.idx[0] += 1; 47 | return Some(vec![c]); 48 | } else { 49 | return None; 50 | } 51 | } 52 | // Keep track of where we are mutating 53 | let mut current_level: usize = self.num_cards - 1; 54 | 55 | while current_level < self.num_cards { 56 | // Move the current level forward one. 57 | self.idx[current_level] += 1; 58 | 59 | // Now check if moving this level forward means that 60 | // We will need more cards to fill out the rest of the hand 61 | // then are there. 62 | let cards_needed_after = self.num_cards - (current_level + 1); 63 | if self.idx[current_level] + cards_needed_after >= self.possible_cards.len() { 64 | if current_level == 0 { 65 | return None; 66 | } 67 | current_level -= 1; 68 | } else { 69 | // If we aren't at the end then 70 | if current_level < self.num_cards - 1 { 71 | self.idx[current_level + 1] = self.idx[current_level]; 72 | } 73 | // Move forward one level 74 | current_level += 1; 75 | } 76 | } 77 | 78 | let result_cards: Vec = self.idx.iter().map(|i| self.possible_cards[*i]).collect(); 79 | Some(result_cards) 80 | } 81 | } 82 | 83 | /// This is useful for trying every possible 5 card hand 84 | /// 85 | /// Probably not something that's going to be done in real 86 | /// use cases, but still not bad. 87 | impl<'a> IntoIterator for &'a FlatDeck { 88 | type Item = Vec; 89 | type IntoIter = CardIter<'a>; 90 | 91 | fn into_iter(self) -> CardIter<'a> { 92 | CardIter::new(&self[..], 5) 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use crate::core::{Deck, FlatHand, Suit, Value}; 99 | 100 | use super::*; 101 | 102 | #[test] 103 | fn test_iter_one() { 104 | let mut h = FlatHand::default(); 105 | h.push(Card { 106 | value: Value::Two, 107 | suit: Suit::Spade, 108 | }); 109 | 110 | for cards in CardIter::new(&h[..], 1) { 111 | assert_eq!(1, cards.len()); 112 | } 113 | assert_eq!(1, CardIter::new(&h[..], 1).count()); 114 | } 115 | 116 | #[test] 117 | fn test_iter_two() { 118 | let mut h = FlatHand::default(); 119 | h.push(Card { 120 | value: Value::Two, 121 | suit: Suit::Spade, 122 | }); 123 | h.push(Card { 124 | value: Value::Three, 125 | suit: Suit::Spade, 126 | }); 127 | h.push(Card { 128 | value: Value::Four, 129 | suit: Suit::Spade, 130 | }); 131 | 132 | // Make sure that we get the correct number back. 133 | assert_eq!(3, CardIter::new(&h[..], 2).count()); 134 | 135 | // Make sure that everything has two cards and they are different. 136 | // 137 | for cards in CardIter::new(&h[..], 2) { 138 | assert_eq!(2, cards.len()); 139 | assert!(cards[0] != cards[1]); 140 | } 141 | } 142 | 143 | #[test] 144 | fn test_iter_deck() { 145 | let d: FlatDeck = Deck::default().into(); 146 | assert_eq!(2_598_960, d.into_iter().count()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/multi_replay_agent.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | extern crate arbitrary; 4 | extern crate libfuzzer_sys; 5 | extern crate rand; 6 | extern crate rs_poker; 7 | 8 | use rand::{rngs::StdRng, SeedableRng}; 9 | 10 | use rs_poker::arena::{ 11 | action::AgentAction, 12 | agent::VecReplayAgent, 13 | // historian::VecHistorian, 14 | test_util::assert_valid_game_state, 15 | test_util::assert_valid_round_data, 16 | Agent, 17 | GameState, 18 | HoldemSimulation, 19 | HoldemSimulationBuilder, 20 | }; 21 | 22 | use libfuzzer_sys::fuzz_target; 23 | 24 | const MIN_BLIND: f32 = 1e-15; 25 | 26 | #[derive(Debug, Clone, arbitrary::Arbitrary)] 27 | struct PlayerInput { 28 | pub stack: f32, 29 | pub actions: Vec, 30 | } 31 | 32 | #[derive(Debug, Clone, arbitrary::Arbitrary)] 33 | struct MultiInput { 34 | pub players: Vec, 35 | pub sb: f32, 36 | pub bb: f32, 37 | pub ante: f32, 38 | pub dealer_idx: usize, 39 | pub seed: u64, 40 | } 41 | 42 | fn build_agent(actions: Vec) -> Box { 43 | Box::::new(VecReplayAgent::new(actions)) 44 | } 45 | 46 | fn input_good(input: &MultiInput) -> bool { 47 | for player in &input.players { 48 | if player.stack.is_nan() || player.stack.is_infinite() || player.stack.is_sign_negative() { 49 | return false; 50 | } 51 | } 52 | 53 | if input.players.len() <= 1 { 54 | return false; 55 | } 56 | 57 | if input.players.len() > 9 { 58 | return false; 59 | } 60 | 61 | // Handle floating point weirdness 62 | if input.ante.is_sign_negative() 63 | || input.ante.is_nan() 64 | || input.ante.is_infinite() 65 | || input.ante < 0.00 66 | { 67 | return false; 68 | } 69 | if input.sb.is_sign_negative() 70 | || input.sb.is_nan() 71 | || input.sb.is_infinite() 72 | || input.sb < input.ante 73 | || input.sb < 0.00 74 | || (input.sb > 0.0 && input.sb < MIN_BLIND) 75 | { 76 | return false; 77 | } 78 | if input.bb.is_sign_negative() 79 | || input.bb.is_nan() 80 | || input.bb.is_infinite() 81 | || input.bb < input.sb 82 | || input.bb < 1.0 83 | || (input.bb > 0.0 && input.bb < MIN_BLIND) 84 | { 85 | return false; 86 | } 87 | 88 | // If we can't post then what's the point? 89 | let min_stack = input 90 | .players 91 | .iter() 92 | .map(|p| p.stack) 93 | .clone() 94 | .reduce(f32::min) 95 | .unwrap_or(0.0); 96 | 97 | if input.bb + input.ante > min_stack { 98 | return false; 99 | } 100 | 101 | if input.bb > 100_000_000.0 { 102 | return false; 103 | } 104 | 105 | // All bet actions are valid 106 | for player in &input.players { 107 | for action in &player.actions { 108 | match action { 109 | AgentAction::Bet(bet) => { 110 | if bet.is_sign_negative() 111 | || bet.is_nan() 112 | || bet.is_infinite() 113 | || (*bet == 0.0 || *bet < input.bb) 114 | { 115 | return false; 116 | } 117 | } 118 | _ => {} 119 | } 120 | } 121 | } 122 | 123 | true 124 | } 125 | 126 | fuzz_target!(|input: MultiInput| { 127 | let sb = input.sb; 128 | let bb = input.sb + input.sb; 129 | let ante = input.ante; 130 | 131 | if !input_good(&input) { 132 | return; 133 | } 134 | 135 | let stacks: Vec = input 136 | .players 137 | .iter() 138 | .map(|pi| (pi.stack).clamp(0.0, 100_000_000.0)) 139 | .collect(); 140 | 141 | let agents: Vec> = input 142 | .players 143 | .into_iter() 144 | .map(|pi| build_agent(pi.actions)) 145 | .collect(); 146 | 147 | let historians: Vec> = vec![ 148 | // Box::new(rs_poker::arena::historian::DirectoryHistorian::new( 149 | // std::path::PathBuf::from("/tmp/fuzz"), 150 | // )), 151 | ]; 152 | 153 | // Create the game state 154 | // Notice that dealer_idx is sanitized to ensure it's in the proper range here 155 | // rather than with the rest of the safety checks. 156 | let game_state = GameState::new_starting(stacks, bb, sb, ante, input.dealer_idx % agents.len()); 157 | let mut rng = StdRng::seed_from_u64(input.seed); 158 | 159 | // let records = VecHistorian::new_storage(); 160 | // let hist = Box::new(VecHistorian::new(records.clone())); 161 | 162 | // Do the thing 163 | let mut sim: HoldemSimulation = HoldemSimulationBuilder::default() 164 | .game_state(game_state) 165 | .agents(agents) 166 | .historians(historians) 167 | .build() 168 | .unwrap(); 169 | sim.run(&mut rng); 170 | 171 | // for _record in records.borrow().iter() { 172 | // // println!("{:?}", record.action); 173 | // } 174 | assert_valid_round_data(&sim.game_state.round_data); 175 | assert_valid_game_state(&sim.game_state); 176 | }); 177 | -------------------------------------------------------------------------------- /src/arena/mod.rs: -------------------------------------------------------------------------------- 1 | //! This is the arena module for simulation via agents. 2 | //! 3 | //! # Single Simulation 4 | //! 5 | //! The tools allow explicit control over the 6 | //! simulation all the way down to the rng. 7 | //! 8 | //! ## Single Simulation Example 9 | //! 10 | //! ``` 11 | //! use rand::{SeedableRng, rngs::StdRng}; 12 | //! use rs_poker::arena::HoldemSimulationBuilder; 13 | //! use rs_poker::arena::agent::CallingAgent; 14 | //! use rs_poker::arena::agent::RandomAgent; 15 | //! use rs_poker::arena::game_state::GameState; 16 | //! 17 | //! let stacks = vec![100.0, 100.0]; 18 | //! let agents: Vec> = vec![ 19 | //! Box::::default(), 20 | //! Box::::default(), 21 | //! ]; 22 | //! let mut rng = StdRng::seed_from_u64(420); 23 | //! 24 | //! let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 25 | //! let mut sim = HoldemSimulationBuilder::default() 26 | //! .game_state(game_state) 27 | //! .agents(agents) 28 | //! .build() 29 | //! .unwrap(); 30 | //! 31 | //! let result = sim.run(&mut rng); 32 | //! ``` 33 | //! 34 | //! # Competition Examples 35 | //! 36 | //! ## `HoldemCompetition` Example 37 | //! 38 | //! It's also possible to run a competition where the 39 | //! same agents compete in multiple simulations 40 | //! with tabulated results 41 | //! 42 | //! ``` 43 | //! use rs_poker::arena::AgentGenerator; 44 | //! use rs_poker::arena::agent::CallingAgentGenerator; 45 | //! use rs_poker::arena::agent::FoldingAgentGenerator; 46 | //! use rs_poker::arena::agent::RandomAgentGenerator; 47 | //! use rs_poker::arena::competition::HoldemCompetition; 48 | //! use rs_poker::arena::competition::StandardSimulationIterator; 49 | //! use rs_poker::arena::game_state::RandomGameStateGenerator; 50 | //! 51 | //! // We are not limited to just heads up. We can have up to full ring of 9 agents. 52 | //! let agent_gens: Vec> = vec![ 53 | //! Box::::default(), 54 | //! Box::::default(), 55 | //! Box::::default(), 56 | //! ]; 57 | //! 58 | //! let game_state_gen = RandomGameStateGenerator::new(3, 100.0, 500.0, 10.0, 5.0, 0.0); 59 | //! let sim_gen = StandardSimulationIterator::new(agent_gens, vec![], game_state_gen); 60 | //! 61 | //! let mut competition = HoldemCompetition::new(sim_gen); 62 | //! 63 | //! let _first_results = competition.run(100).unwrap(); 64 | //! let recent_results = competition.run(100).unwrap(); 65 | //! 66 | //! // The holdem competition tabulates the results accross multiple runs. 67 | //! println!("{:?}", recent_results); 68 | //! ``` 69 | //! 70 | //! ## `SingleTableTournament` Example 71 | //! 72 | //! It's also possible to run a single table tournament where the 73 | //! game state continues on until one player has all the money. 74 | //! 75 | //! ``` 76 | //! use rs_poker::arena::AgentGenerator; 77 | //! use rs_poker::arena::agent::RandomAgentGenerator; 78 | //! use rs_poker::arena::competition::SingleTableTournamentBuilder; 79 | //! use rs_poker::arena::game_state::GameState; 80 | //! 81 | //! // We are not limited to just heads up. We can have up to full ring of 9 agents. 82 | //! let agent_gens: Vec> = vec![ 83 | //! Box::::default(), 84 | //! Box::::default(), 85 | //! Box::::default(), 86 | //! Box::::default(), 87 | //! ]; 88 | //! let stacks = vec![100.0; 4]; 89 | //! 90 | //! // This is the starting game state. 91 | //! let game_state = GameState::new_starting(stacks, 10.0, 5.0, 1.0, 0); 92 | //! 93 | //! let tournament = SingleTableTournamentBuilder::default() 94 | //! .agent_generators(agent_gens) 95 | //! .starting_game_state(game_state) 96 | //! .build() 97 | //! .unwrap(); 98 | //! 99 | //! let results = tournament.run().unwrap(); 100 | //! ``` 101 | //! 102 | //! ## Counter Factual Regret Minimization (CFR) Example 103 | //! 104 | //! rs-poker has an implementation of CFR that can be used to implement agents 105 | //! that decide their actions based on the regret minimization algorithm. For 106 | //! that you can use the `CFRAgent` along with the `CFRHistorian` and `CFRState` 107 | //! structs. 108 | //! 109 | //! The strategy is implemented by the `ActionGenerator` trait, which is used to 110 | //! generate potential actions for a given game state. The 111 | //! `BasicCFRActionGenerator` is a simple implementation that generates fold, 112 | //! call, and All-In actions. 113 | //! 114 | //! The `FixedGameStateIteratorGen` is an implementation of the 115 | //! `GameStateIteratorGen` that gives possible game states to the agent. It 116 | //! generates hands that are evaluated for the reward that the agent will get. 117 | //! 118 | //! The Agent then chooses the action based upon the regret minimization. 119 | //! 120 | //! ``` 121 | //! use rs_poker::arena::cfr::CFRAgent; 122 | //! ``` 123 | pub mod action; 124 | pub mod agent; 125 | pub mod cfr; 126 | pub mod competition; 127 | pub mod errors; 128 | pub mod game_state; 129 | pub mod historian; 130 | pub mod sim_builder; 131 | pub mod simulation; 132 | 133 | #[cfg(any(test, feature = "arena-test-util"))] 134 | pub mod test_util; 135 | 136 | pub use agent::{Agent, AgentGenerator, CloneAgentGenerator}; 137 | pub use game_state::{CloneGameStateGenerator, GameState, GameStateGenerator}; 138 | pub use historian::{CloneHistorianGenerator, Historian, HistorianError, HistorianGenerator}; 139 | pub use sim_builder::HoldemSimulationBuilder; 140 | pub use simulation::HoldemSimulation; 141 | -------------------------------------------------------------------------------- /examples/exports/cfr_tree.dot: -------------------------------------------------------------------------------- 1 | digraph CFRTree { 2 | // Graph styling 3 | graph [rankdir=TB, splines=polyline, nodesep=1.0, ranksep=1.2, concentrate=true, compound=true]; 4 | node [shape=box, style="rounded,filled", fontname="Arial", margin=0.2]; 5 | edge [fontname="Arial", penwidth=1.0, labelangle=25, labeldistance=1.8, labelfloat=true]; 6 | // Add legend 7 | subgraph cluster_legend { 8 | graph [rank=sink]; 9 | label="Legend"; 10 | style=rounded; 11 | color=gray; 12 | margin=16; 13 | node [shape=plaintext, style=""]; 14 | legend [label=< 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
Node Types:
• Root (⬢): Light Blue - Starting state
• Player (□): Coral - Decision points
• Chance (○): Light Green - Card deals
• Terminal (⬡): Light Grey - Final states

Edge Properties:
• Thickness: Usage frequency
• Labels: Action/Card
• Percent: Visit frequency
27 | >]; 28 | } 29 | 30 | // Node grouping 31 | {rank=source; node_0;} 32 | node_0 [label="Root Node\nIndex: 0\nTotal Visits: 0", shape=doubleoctagon, style="filled", fillcolor="lightblue", tooltip="Most Common Action: 51\nAction Frequency: 0.0%"]; 33 | node_0 -> node_1 [label="0", weight=1] 34 | node_1 [label="Player 0 Node\nIndex: 1\nTotal Visits: 3", shape=box, style="rounded,filled", fillcolor="coral", tooltip="Most Common Action: 2\nAction Frequency: 66.7%"]; 35 | {rank=same; node_1;} // Group player nodes 36 | node_1 -> node_2 [label="Fold", penwidth=1, color="#9B9BFF", tooltip="Frequency: 0.0%", xlabel="0%", weight=1] 37 | node_1 -> node_3 [label="Check/Call", penwidth=4.3333335, color="#BCBCFF", tooltip="Frequency: 33.3%", xlabel="33%", weight=33] 38 | node_1 -> node_4 [label="Bet/Raise 1", penwidth=7.666667, color="#DDDDFF", tooltip="Frequency: 66.7%", xlabel="67%", weight=66] 39 | node_2 [label="Terminal Node\nIndex: 2\nUtility: -10.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 40 | node_3 [label="Chance Node\nIndex: 3\nTotal Visits: 0", shape=ellipse, style="filled", fillcolor="lightgreen", tooltip="Most Common Action: 51\nAction Frequency: 0.0%"]; 41 | node_3 -> node_5 [label="2s", weight=1] 42 | node_3 -> node_8 [label="3s", weight=1] 43 | node_3 -> node_11 [label="4s", weight=1] 44 | node_4 [label="Chance Node\nIndex: 4\nTotal Visits: 0", shape=ellipse, style="filled", fillcolor="lightgreen", tooltip="Most Common Action: 51\nAction Frequency: 0.0%"]; 45 | node_4 -> node_14 [label="2s", weight=1] 46 | node_5 [label="Player 0 Node\nIndex: 5\nTotal Visits: 0", shape=box, style="rounded,filled", fillcolor="coral", tooltip="Most Common Action: 51\nAction Frequency: 0.0%"]; 47 | {rank=same; node_5;} // Group player nodes 48 | node_5 -> node_6 [label="Fold", weight=1] 49 | node_5 -> node_7 [label="Check/Call", weight=1] 50 | node_6 [label="Terminal Node\nIndex: 6\nUtility: 15.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 51 | node_7 [label="Terminal Node\nIndex: 7\nUtility: 5.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 52 | node_8 [label="Player 1 Node\nIndex: 8\nTotal Visits: 0", shape=box, style="rounded,filled", fillcolor="coral", tooltip="Most Common Action: 51\nAction Frequency: 0.0%"]; 53 | {rank=same; node_8;} // Group player nodes 54 | node_8 -> node_9 [label="Fold", weight=1] 55 | node_8 -> node_10 [label="Check/Call", weight=1] 56 | node_9 [label="Terminal Node\nIndex: 9\nUtility: 15.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 57 | node_10 [label="Terminal Node\nIndex: 10\nUtility: 5.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 58 | node_11 [label="Player 0 Node\nIndex: 11\nTotal Visits: 0", shape=box, style="rounded,filled", fillcolor="coral", tooltip="Most Common Action: 51\nAction Frequency: 0.0%"]; 59 | {rank=same; node_11;} // Group player nodes 60 | node_11 -> node_12 [label="Fold", weight=1] 61 | node_11 -> node_13 [label="Check/Call", weight=1] 62 | node_12 [label="Terminal Node\nIndex: 12\nUtility: 15.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 63 | node_13 [label="Terminal Node\nIndex: 13\nUtility: 5.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 64 | node_14 [label="Player 1 Node\nIndex: 14\nTotal Visits: 0", shape=box, style="rounded,filled", fillcolor="coral", tooltip="Most Common Action: 51\nAction Frequency: 0.0%"]; 65 | {rank=same; node_14;} // Group player nodes 66 | node_14 -> node_15 [label="Fold", weight=1] 67 | node_14 -> node_16 [label="Check/Call", weight=1] 68 | node_15 [label="Terminal Node\nIndex: 15\nUtility: 20.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 69 | node_16 [label="Chance Node\nIndex: 16\nTotal Visits: 0", shape=ellipse, style="filled", fillcolor="lightgreen", tooltip="Most Common Action: 51\nAction Frequency: 0.0%"]; 70 | node_16 -> node_17 [label="2s", weight=1] 71 | node_17 [label="Terminal Node\nIndex: 17\nUtility: 30.00\nVisits: 0", shape=hexagon, style="filled", fillcolor="lightgrey", tooltip="Average Utility: 0.00"]; 72 | } 73 | -------------------------------------------------------------------------------- /src/arena/test_util.rs: -------------------------------------------------------------------------------- 1 | use std::assert_matches::assert_matches; 2 | 3 | use approx::assert_relative_eq; 4 | 5 | use crate::arena::game_state::Round; 6 | 7 | use super::action::AgentAction; 8 | use super::{GameState, game_state::RoundData}; 9 | 10 | use crate::arena::action::Action; 11 | use crate::arena::historian::HistoryRecord; 12 | 13 | pub fn assert_valid_round_data(round_data: &RoundData) { 14 | // Get all of the player still active at the end of the round. 15 | // for any round with bets they should have called. 16 | // 17 | // EG no one should call for less than the max and still be in. 18 | let active_bets: Vec = round_data 19 | .player_bet 20 | .iter() 21 | .enumerate() 22 | .filter(|(idx, _)| round_data.needs_action.get(*idx)) 23 | .map(|(_, bet)| *bet) 24 | .collect(); 25 | 26 | let max_active = active_bets.clone().into_iter().reduce(f32::max); 27 | 28 | if let Some(max) = max_active { 29 | for bet in active_bets.into_iter() { 30 | assert_eq!( 31 | bet, max, 32 | "Players still active should have called the max bet, round_data: {round_data:?}" 33 | ); 34 | } 35 | } 36 | } 37 | 38 | pub fn assert_valid_game_state(game_state: &GameState) { 39 | assert_eq!(Round::Complete, game_state.round); 40 | 41 | let should_have_bets = game_state.ante + game_state.small_blind + game_state.big_blind > 0.0; 42 | 43 | let total_bet = game_state.player_bet.iter().copied().sum(); 44 | 45 | if should_have_bets { 46 | let any_above_zero = game_state.player_bet.iter().any(|bet| *bet > 0.0); 47 | 48 | assert!( 49 | any_above_zero, 50 | "At least one player should have a bet, game_state: {:?}", 51 | game_state.player_bet 52 | ); 53 | 54 | assert_ne!(0.0, total_bet); 55 | } 56 | 57 | let epsilon = total_bet / 100_000.0; 58 | assert_relative_eq!(total_bet, game_state.total_pot, epsilon = epsilon); 59 | 60 | let total_winning: f32 = game_state.player_winnings.iter().copied().sum(); 61 | 62 | assert_relative_eq!(total_winning, total_bet, epsilon = epsilon); 63 | assert_relative_eq!(total_winning, game_state.total_pot, epsilon = epsilon); 64 | 65 | // The dealer has to be well specified. 66 | assert!(game_state.dealer_idx < game_state.num_players); 67 | 68 | // The board should be full or getting full 69 | assert!(game_state.board.len() <= 5); 70 | 71 | assert!(game_state.small_blind <= game_state.big_blind); 72 | 73 | for idx in 0..game_state.num_players { 74 | // If they aren't active (folded) 75 | // and aren't all in then they shouldn't win anything 76 | if !game_state.player_active.get(idx) && !game_state.player_all_in.get(idx) { 77 | assert_eq!(0.0, game_state.player_winnings[idx]); 78 | } 79 | } 80 | } 81 | 82 | pub fn assert_valid_history(history_storage: &[HistoryRecord]) { 83 | // There should always be some history 84 | assert!(!history_storage.is_empty()); 85 | 86 | // The first action should always be a game start 87 | assert_matches!(history_storage[0].action, Action::GameStart(_)); 88 | 89 | // History should include round advance to complete 90 | assert_advances_to_complete(history_storage); 91 | 92 | assert_round_contains_valid_player_actions(history_storage); 93 | 94 | assert_no_player_actions_after_fold(history_storage); 95 | } 96 | 97 | fn assert_advances_to_complete(history_storage: &[HistoryRecord]) { 98 | let round_advances: Vec<&Action> = history_storage 99 | .iter() 100 | .filter(|record| matches!(record.action, Action::RoundAdvance(Round::Complete))) 101 | .map(|record| &record.action) 102 | .collect(); 103 | 104 | assert_eq!(1, round_advances.len()); 105 | } 106 | 107 | fn assert_round_contains_valid_player_actions(history_storage: &[HistoryRecord]) { 108 | // For Preflop, Flop, Turn, and River there should 109 | // be a at least one player action for each player 110 | // unless everyone else has folded or they are all in. 111 | for round in &[Round::Preflop, Round::Flop, Round::Turn, Round::River] { 112 | let advance_history = history_storage.iter().find(|record| { 113 | if let Action::RoundAdvance(found_round) = &record.action { 114 | found_round == round 115 | } else { 116 | false 117 | } 118 | }); 119 | 120 | if advance_history.is_none() { 121 | continue; 122 | } 123 | // TODO check here for 124 | } 125 | } 126 | 127 | fn assert_no_player_actions_after_fold(history_storage: &[HistoryRecord]) { 128 | // If a player has folded 129 | // they shouldn't have any actions after that. 130 | let player_fold_index: Vec<(usize, usize)> = history_storage 131 | .iter() 132 | .enumerate() 133 | .filter_map(|(index, record)| { 134 | if let Action::PlayedAction(action) = &record.action { 135 | if action.action == AgentAction::Fold { 136 | Some((action.idx, index)) 137 | } else { 138 | None 139 | } 140 | } else { 141 | None 142 | } 143 | }) 144 | .collect(); 145 | 146 | for (player_idx, fold_index) in player_fold_index { 147 | let actions_after_fold = history_storage 148 | .iter() 149 | .skip(fold_index + 1) 150 | .filter(|record| { 151 | if let Action::PlayedAction(action) = &record.action { 152 | action.idx == player_idx 153 | } else { 154 | false 155 | } 156 | }); 157 | 158 | assert_eq!(0, actions_after_fold.count()); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/arena/historian/vec.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use crate::arena::{GameState, action::Action}; 4 | 5 | use super::{Historian, HistorianError}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct HistoryRecord { 9 | pub before_game_state: Option, 10 | pub action: Action, 11 | pub after_game_state: GameState, 12 | } 13 | 14 | /// VecHistorian is a historian that will 15 | /// append each action to a vector. 16 | pub struct VecHistorian { 17 | previous: Option, 18 | records: Rc>>, 19 | } 20 | 21 | impl VecHistorian { 22 | /// Create a new storage for the historian 23 | /// that can be introspected later. 24 | pub fn get_storage(&self) -> Rc>> { 25 | self.records.clone() 26 | } 27 | 28 | /// Create a new VecHistorian with the provided storage 29 | /// `Rc>>` 30 | pub fn new_with_actions(actions: Rc>>) -> Self { 31 | Self { 32 | records: actions, 33 | previous: None, 34 | } 35 | } 36 | 37 | pub fn new() -> Self { 38 | VecHistorian::new_with_actions(Rc::new(RefCell::new(vec![]))) 39 | } 40 | } 41 | 42 | impl Default for VecHistorian { 43 | fn default() -> Self { 44 | VecHistorian::new() 45 | } 46 | } 47 | 48 | impl Historian for VecHistorian { 49 | fn record_action( 50 | &mut self, 51 | _id: u128, 52 | game_state: &GameState, 53 | action: Action, 54 | ) -> Result<(), HistorianError> { 55 | let mut act = self.records.try_borrow_mut()?; 56 | 57 | // Now that we have the lock, we can record the action 58 | act.push(HistoryRecord { 59 | before_game_state: self.previous.clone(), 60 | action, 61 | after_game_state: game_state.clone(), 62 | }); 63 | 64 | // Record the game state for the next action 65 | self.previous = Some(game_state.clone()); 66 | Ok(()) 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use crate::arena::{ 73 | Agent, HoldemSimulationBuilder, 74 | agent::{CallingAgent, RandomAgent}, 75 | }; 76 | 77 | use super::*; 78 | 79 | #[test] 80 | fn test_vec_historian() { 81 | let hist = Box::new(VecHistorian::default()); 82 | let records = hist.get_storage(); 83 | let mut rng = rand::rng(); 84 | 85 | let stacks = vec![100.0; 5]; 86 | let agents: Vec> = vec![ 87 | Box::::default(), 88 | Box::::default(), 89 | Box::::default(), 90 | Box::::default(), 91 | Box::::default(), 92 | ]; 93 | let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 94 | 95 | let mut sim = HoldemSimulationBuilder::default() 96 | .game_state(game_state) 97 | .agents(agents) 98 | .historians(vec![hist]) 99 | .build() 100 | .unwrap(); 101 | 102 | sim.run(&mut rng); 103 | 104 | assert!(records.borrow().len() > 10); 105 | } 106 | 107 | #[test] 108 | fn test_restarting_simulations() { 109 | // The first records. 110 | let hist = Box::new(VecHistorian::default()); 111 | let records = hist.get_storage(); 112 | let mut rng = rand::rng(); 113 | 114 | let stacks = vec![100.0; 2]; 115 | let agents: Vec> = vec![ 116 | Box::::default(), 117 | Box::::default(), 118 | ]; 119 | 120 | let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 121 | 122 | let mut sim = HoldemSimulationBuilder::default() 123 | .game_state(game_state) 124 | .agents(agents) 125 | .historians(vec![hist]) 126 | .build() 127 | .unwrap(); 128 | 129 | sim.run(&mut rng); 130 | 131 | // Now we have a set of records of what happenend in the first simulation. 132 | // It doesn't matter what actions the agents took, we just need to know that 133 | // the simulation always restarts at asking the same player to play 134 | 135 | for r in records.borrow().iter() { 136 | if let (Action::PlayedAction(played_action), Some(before_game_state)) = 137 | (&r.action, &r.before_game_state) 138 | { 139 | let inner_agents: Vec> = vec![ 140 | Box::::default(), 141 | Box::::default(), 142 | ]; 143 | 144 | let inner_hist = Box::new(VecHistorian::default()); 145 | let inner_records = inner_hist.get_storage(); 146 | 147 | // We can now restart the simulation and see if the same player takes the next 148 | // turn 149 | let mut inner_sim = HoldemSimulationBuilder::default() 150 | .game_state(before_game_state.clone()) 151 | .agents(inner_agents) 152 | .historians(vec![inner_hist]) 153 | .build() 154 | .unwrap(); 155 | 156 | inner_sim.run(&mut rng); 157 | 158 | let first_record = inner_records.borrow().first().unwrap().clone(); 159 | 160 | if let Action::PlayedAction(inner_played_action) = first_record.action { 161 | assert_eq!(played_action.idx, inner_played_action.idx); 162 | } else { 163 | panic!( 164 | "The first action should be a played action, found {:?}", 165 | first_record.action 166 | ); 167 | } 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/arena/cfr/gamestate_iterator_gen.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{GameState, game_state}; 2 | 3 | /// This trait defines an interface for types that can generate an iterator 4 | /// over possible game states from a given initial game state. 5 | pub trait GameStateIteratorGen { 6 | fn generate(&self, game_state: &GameState) -> impl Iterator; 7 | } 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct FixedGameStateIteratorGen { 11 | pub num_hands: usize, 12 | } 13 | 14 | impl FixedGameStateIteratorGen { 15 | pub fn new(num_hands: usize) -> Self { 16 | Self { num_hands } 17 | } 18 | } 19 | 20 | impl Default for FixedGameStateIteratorGen { 21 | fn default() -> Self { 22 | Self { num_hands: 10 } 23 | } 24 | } 25 | 26 | /// Creates an iterator that generates `num_hands` clones of the input game 27 | /// state. 28 | /// 29 | /// This implementation of [`GameStateIteratorGen`] creates a simple iterator 30 | /// that produces exact copies of the input game state. The number of copies is 31 | /// determined by the `num_hands` field set during construction. 32 | /// 33 | /// # Arguments 34 | /// 35 | /// * `game_state` - The game state to be cloned 36 | /// 37 | /// # Returns 38 | /// 39 | /// Returns an iterator that yields `num_hands` clones of the input `game_state` 40 | impl GameStateIteratorGen for FixedGameStateIteratorGen { 41 | fn generate(&self, game_state: &GameState) -> impl Iterator { 42 | (0..self.num_hands).map(|_| game_state.clone()) 43 | } 44 | } 45 | 46 | #[derive(Clone, Debug)] 47 | pub struct PerRoundFixedGameStateIteratorGen { 48 | // For PreFlop 49 | pub pre_flop_num_hands: usize, 50 | // For Flop 51 | pub flop_num_hands: usize, 52 | // For Turn 53 | pub turn_num_hands: usize, 54 | // For River 55 | pub river_num_hands: usize, 56 | } 57 | 58 | impl PerRoundFixedGameStateIteratorGen { 59 | pub fn new( 60 | pre_flop_num_hands: usize, 61 | flop_num_hands: usize, 62 | turn_num_hands: usize, 63 | river_num_hands: usize, 64 | ) -> Self { 65 | Self { 66 | pre_flop_num_hands, 67 | flop_num_hands, 68 | turn_num_hands, 69 | river_num_hands, 70 | } 71 | } 72 | 73 | fn num_hands(&self, game_state: &GameState) -> usize { 74 | match game_state.round { 75 | game_state::Round::Preflop => self.pre_flop_num_hands, 76 | game_state::Round::Flop => self.flop_num_hands, 77 | game_state::Round::Turn => self.turn_num_hands, 78 | game_state::Round::River => self.river_num_hands, 79 | _ => 1, // Handle any other rounds if necessary 80 | } 81 | } 82 | } 83 | 84 | impl Default for PerRoundFixedGameStateIteratorGen { 85 | fn default() -> Self { 86 | Self { 87 | pre_flop_num_hands: 10, 88 | flop_num_hands: 10, 89 | turn_num_hands: 10, 90 | river_num_hands: 1, 91 | } 92 | } 93 | } 94 | 95 | impl GameStateIteratorGen for PerRoundFixedGameStateIteratorGen { 96 | fn generate(&self, game_state: &GameState) -> impl Iterator { 97 | (0..self.num_hands(game_state)).map(|_| game_state.clone()) 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use super::*; 104 | 105 | #[test] 106 | fn test_simple() { 107 | let game_state = GameState::new_starting(vec![100.0; 3], 10.0, 5.0, 0.0, 0); 108 | let generator = FixedGameStateIteratorGen::new(3); 109 | let mut iter = generator.generate(&game_state); 110 | 111 | assert_eq!(iter.next().unwrap(), game_state); 112 | assert_eq!(iter.next().unwrap(), game_state); 113 | assert_eq!(iter.next().unwrap(), game_state); 114 | assert!(iter.next().is_none()); 115 | } 116 | 117 | #[test] 118 | fn test_per_round() { 119 | let mut game_state = GameState::new_starting(vec![100.0; 3], 10.0, 5.0, 0.0, 0); 120 | let generator = PerRoundFixedGameStateIteratorGen::new(2, 3, 4, 1); 121 | 122 | game_state.advance_round(); 123 | game_state.advance_round(); 124 | game_state.advance_round(); 125 | 126 | assert_eq!(game_state.round, game_state::Round::Preflop); 127 | 128 | // Preflop 129 | { 130 | let mut iter = generator.generate(&game_state); 131 | assert_eq!(iter.next().unwrap(), game_state); 132 | assert_eq!(iter.next().unwrap(), game_state); 133 | assert!(iter.next().is_none()); 134 | } 135 | 136 | // Flop 137 | game_state.advance_round(); 138 | game_state.advance_round(); 139 | assert_eq!(game_state.round, game_state::Round::Flop); 140 | { 141 | let mut iter_flop = generator.generate(&game_state); 142 | assert_eq!(iter_flop.next().unwrap(), game_state); 143 | assert_eq!(iter_flop.next().unwrap(), game_state); 144 | assert_eq!(iter_flop.next().unwrap(), game_state); 145 | assert!(iter_flop.next().is_none()); 146 | } 147 | 148 | // Turn 149 | game_state.advance_round(); 150 | game_state.advance_round(); 151 | assert_eq!(game_state.round, game_state::Round::Turn); 152 | { 153 | let mut iter_turn = generator.generate(&game_state); 154 | 155 | assert_eq!(iter_turn.next().unwrap(), game_state); 156 | assert_eq!(iter_turn.next().unwrap(), game_state); 157 | assert_eq!(iter_turn.next().unwrap(), game_state); 158 | assert_eq!(iter_turn.next().unwrap(), game_state); 159 | assert!(iter_turn.next().is_none()); 160 | } 161 | 162 | // River 163 | game_state.advance_round(); 164 | game_state.advance_round(); 165 | assert_eq!(game_state.round, game_state::Round::River); 166 | { 167 | let mut iter_river = generator.generate(&game_state); 168 | 169 | assert_eq!(iter_river.next().unwrap(), game_state); 170 | assert!(iter_river.next().is_none()); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/core/flat_deck.rs: -------------------------------------------------------------------------------- 1 | use crate::core::card::Card; 2 | use crate::core::deck::Deck; 3 | use std::ops::{Index, Range, RangeFrom, RangeFull, RangeTo}; 4 | 5 | extern crate rand; 6 | use rand::Rng; 7 | use rand::rng; 8 | use rand::seq::{IndexedRandom, SliceRandom}; 9 | 10 | /// `FlatDeck` is a deck of cards that allows easy 11 | /// indexing into the cards. It does not provide 12 | /// contains methods. 13 | #[derive(Debug, Clone, PartialEq)] 14 | pub struct FlatDeck { 15 | /// Card storage. 16 | cards: Vec, 17 | } 18 | 19 | impl FlatDeck { 20 | /// How many cards are there in the deck ? 21 | pub fn len(&self) -> usize { 22 | self.cards.len() 23 | } 24 | /// Have all cards been dealt ? 25 | /// This probably won't be used as it's unlikely 26 | /// that someone will deal all 52 cards from a deck. 27 | pub fn is_empty(&self) -> bool { 28 | self.cards.is_empty() 29 | } 30 | 31 | /// Add a card to the deck. 32 | /// This does not check if the card is already in the deck. 33 | /// It will just add it to the end of the deck. 34 | /// 35 | /// # Examples 36 | /// 37 | /// ```rust 38 | /// use rs_poker::core::{Card, Deck, FlatDeck, Suit, Value}; 39 | /// 40 | /// let mut deck: FlatDeck = Deck::new().into(); 41 | /// let card = Card::new(Value::Ace, Suit::Club); 42 | /// deck.push(card); 43 | /// 44 | /// assert_eq!(1, deck.len()); 45 | /// assert_eq!(card, deck.deal().unwrap()); 46 | /// ``` 47 | pub fn push(&mut self, c: Card) { 48 | self.cards.push(c); 49 | } 50 | 51 | /// Give a random sample of the cards still left in the deck 52 | pub fn sample(&self, n: usize) -> Vec { 53 | let mut rng = rng(); 54 | self.cards.choose_multiple(&mut rng, n).cloned().collect() 55 | } 56 | 57 | /// Randomly shuffle the flat deck. 58 | /// This will ensure the there's no order to the deck. 59 | pub fn shuffle(&mut self, rng: &mut R) { 60 | self.cards.shuffle(rng) 61 | } 62 | 63 | /// Deal a card if there is one there to deal. 64 | /// None if the deck is empty 65 | pub fn deal(&mut self) -> Option { 66 | self.cards.pop() 67 | } 68 | } 69 | 70 | impl Index for FlatDeck { 71 | type Output = Card; 72 | fn index(&self, index: usize) -> &Card { 73 | &self.cards[index] 74 | } 75 | } 76 | impl Index> for FlatDeck { 77 | type Output = [Card]; 78 | fn index(&self, index: Range) -> &[Card] { 79 | &self.cards[index] 80 | } 81 | } 82 | impl Index> for FlatDeck { 83 | type Output = [Card]; 84 | fn index(&self, index: RangeTo) -> &[Card] { 85 | &self.cards[index] 86 | } 87 | } 88 | impl Index> for FlatDeck { 89 | type Output = [Card]; 90 | fn index(&self, index: RangeFrom) -> &[Card] { 91 | &self.cards[index] 92 | } 93 | } 94 | impl Index for FlatDeck { 95 | type Output = [Card]; 96 | fn index(&self, index: RangeFull) -> &[Card] { 97 | &self.cards[index] 98 | } 99 | } 100 | 101 | impl From> for FlatDeck { 102 | fn from(value: Vec) -> Self { 103 | Self { cards: value } 104 | } 105 | } 106 | 107 | /// Allow creating a flat deck from a Deck 108 | impl From for FlatDeck { 109 | /// Flatten this deck, consuming it to produce a `FlatDeck` that's 110 | /// easier to get random access to. 111 | fn from(value: Deck) -> Self { 112 | // We sort the cards so that the same input 113 | // cards always result in the same starting flat deck 114 | let mut cards: Vec = value.into_iter().collect(); 115 | cards.sort(); 116 | Self { cards } 117 | } 118 | } 119 | impl Default for FlatDeck { 120 | fn default() -> Self { 121 | let mut cards: Vec = Deck::default().into_iter().collect(); 122 | let mut rng = rng(); 123 | cards.shuffle(&mut rng); 124 | Self { cards } 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use rand::{SeedableRng, rngs::StdRng}; 131 | 132 | use super::*; 133 | use crate::core::card::{Suit, Value}; 134 | 135 | #[test] 136 | fn test_deck_from() { 137 | let fd: FlatDeck = Deck::default().into(); 138 | assert_eq!(52, fd.len()); 139 | } 140 | 141 | #[test] 142 | fn test_from_vec() { 143 | let c = Card { 144 | value: Value::Nine, 145 | suit: Suit::Heart, 146 | }; 147 | let v = vec![c]; 148 | 149 | let mut flat_deck: FlatDeck = v.into(); 150 | 151 | assert_eq!(1, flat_deck.len()); 152 | assert_eq!(c, flat_deck.deal().unwrap()); 153 | } 154 | 155 | #[test] 156 | fn test_shuffle_rng() { 157 | let mut fd_one: FlatDeck = Deck::default().into(); 158 | let mut fd_two: FlatDeck = Deck::default().into(); 159 | 160 | let mut rng_one = StdRng::seed_from_u64(420); 161 | let mut rng_two = StdRng::seed_from_u64(420); 162 | 163 | fd_one.shuffle(&mut rng_one); 164 | fd_two.shuffle(&mut rng_two); 165 | 166 | assert_eq!(fd_one, fd_two); 167 | assert_eq!(fd_one, fd_two); 168 | } 169 | 170 | #[test] 171 | fn test_index() { 172 | let mut fd: FlatDeck = Deck::new().into(); 173 | 174 | let c = Card { 175 | value: Value::Nine, 176 | suit: Suit::Heart, 177 | }; 178 | fd.push(c); 179 | assert_eq!(c, fd[0]); 180 | 181 | let mut fd: FlatDeck = Deck::new().into(); 182 | let c = Card { 183 | value: Value::Nine, 184 | suit: Suit::Heart, 185 | }; 186 | let c2 = Card { 187 | value: Value::Ten, 188 | suit: Suit::Heart, 189 | }; 190 | fd.push(c); 191 | fd.push(c2); 192 | assert_eq!(c, fd[0]); 193 | assert_eq!(c2, fd[1]); 194 | } 195 | 196 | #[test] 197 | fn test_is_empty() { 198 | let mut fd: FlatDeck = Deck::new().into(); 199 | assert!(fd.is_empty()); 200 | 201 | fd.push(Card { 202 | value: Value::Nine, 203 | suit: Suit::Heart, 204 | }); 205 | assert!(!fd.is_empty()); 206 | let dealt_card = fd.deal(); 207 | 208 | assert!(fd.is_empty()); 209 | assert_eq!( 210 | Some(Card { 211 | value: Value::Nine, 212 | suit: Suit::Heart, 213 | }), 214 | dealt_card 215 | ); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/arena/competition/holdem_competition.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, VecDeque}, 3 | fmt::Debug, 4 | }; 5 | 6 | use crate::arena::{HoldemSimulation, errors::HoldemSimulationError, game_state::Round}; 7 | 8 | /// A struct to help seeing which agent is likely to do well 9 | /// 10 | /// Each competition is a series of `HoldemSimulations` 11 | /// from the `HoldemSimulationGenerator` passed in. 12 | pub struct HoldemCompetition> { 13 | simulation_iterator: T, 14 | /// The number of rounds that have been run. 15 | pub num_rounds: usize, 16 | 17 | /// stack size change normalized in big blinds 18 | pub total_change: Vec, 19 | pub max_change: Vec, 20 | pub min_change: Vec, 21 | 22 | /// How many hands each agent has made some profit 23 | pub win_count: Vec, 24 | /// How many hands the agents have lost money 25 | pub loss_count: Vec, 26 | // How many times the agent has lost no money 27 | pub zero_count: Vec, 28 | // Count of the round before the simulation stopped 29 | pub before_count: HashMap, 30 | 31 | /// Maximum number of HoldemSimulation's to 32 | /// keep in a long call to `run` 33 | max_sim_history: usize, 34 | } 35 | 36 | const MAX_PLAYERS: usize = 12; 37 | 38 | impl> HoldemCompetition { 39 | /// Creates a new HoldemHandCompetition instance with the provided 40 | /// HoldemSimulation. 41 | /// 42 | /// Initializes the number of rounds to 0 and the stack change vectors to 0 43 | /// for each agent. 44 | pub fn new(simulation_iterator: T) -> HoldemCompetition { 45 | HoldemCompetition { 46 | simulation_iterator, 47 | max_sim_history: 100, 48 | // Set everything to zero 49 | num_rounds: 0, 50 | total_change: vec![0.0; MAX_PLAYERS], 51 | min_change: vec![0.0; MAX_PLAYERS], 52 | max_change: vec![0.0; MAX_PLAYERS], 53 | win_count: vec![0; MAX_PLAYERS], 54 | loss_count: vec![0; MAX_PLAYERS], 55 | zero_count: vec![0; MAX_PLAYERS], 56 | // Round before stopping 57 | before_count: HashMap::new(), 58 | } 59 | } 60 | 61 | pub fn run( 62 | &mut self, 63 | num_rounds: usize, 64 | ) -> Result, HoldemSimulationError> { 65 | let mut sims = VecDeque::with_capacity(self.max_sim_history); 66 | let mut rand = rand::rng(); 67 | 68 | for _round in 0..num_rounds { 69 | // Createa a new holdem simulation 70 | let mut running_sim = self.simulation_iterator.next().unwrap(); 71 | // Run the sim 72 | running_sim.run(&mut rand); 73 | // Update the stack change stats 74 | self.update_metrics(&running_sim); 75 | // Update the counter 76 | self.num_rounds += 1; 77 | // If there are too many sims in the circular queue then make some space 78 | if sims.len() >= self.max_sim_history { 79 | sims.pop_front(); 80 | } 81 | // Store the final results 82 | sims.push_back(running_sim); 83 | } 84 | 85 | // Drain the whole vecdequeue 86 | Ok(sims.into_iter().collect()) 87 | } 88 | 89 | fn update_metrics(&mut self, running_sim: &HoldemSimulation) { 90 | // Calculates the change in each player's winnings for the round, 91 | // normalized by the big blind amount. 92 | // 93 | // TODO: we need to filter out the players that never started the hand. 94 | let changes = running_sim 95 | .game_state 96 | .starting_stacks 97 | .iter() 98 | .zip(running_sim.game_state.stacks.iter()) 99 | .enumerate() 100 | .map(|(idx, (starting, ending))| { 101 | ( 102 | idx, 103 | (*ending - *starting) / running_sim.game_state.big_blind, 104 | ) 105 | }); 106 | 107 | for (idx, norm_change) in changes { 108 | // Running total 109 | self.total_change[idx] += norm_change; 110 | // What's the most we lose 111 | self.min_change[idx] = self.min_change[idx].min(norm_change); 112 | // What's the most we win 113 | self.max_change[idx] = self.max_change[idx].max(norm_change); 114 | 115 | // Count how many times the agent wins or loses 116 | if norm_change > 0.0 { 117 | self.win_count[idx] += 1; 118 | } else if norm_change < 0.0 { 119 | self.loss_count[idx] += 1; 120 | } else { 121 | self.zero_count[idx] += 1; 122 | } 123 | } 124 | // Update the count 125 | let count = self 126 | .before_count 127 | .entry(running_sim.game_state.round_before) 128 | .or_default(); 129 | *count += 1; 130 | } 131 | } 132 | 133 | impl> Debug for HoldemCompetition { 134 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 135 | f.debug_struct("HoldemCompetition") 136 | .field("num_rounds", &self.num_rounds) 137 | .field("total_change", &self.total_change) 138 | .field("max_change", &self.max_change) 139 | .field("min_change", &self.min_change) 140 | .field("win_count", &self.win_count) 141 | .field("zero_count", &self.zero_count) 142 | .field("loss_count", &self.loss_count) 143 | .field("round_before", &self.before_count) 144 | .finish() 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use crate::arena::{ 151 | AgentGenerator, CloneGameStateGenerator, GameState, 152 | agent::{CallingAgentGenerator, RandomAgentGenerator}, 153 | competition::StandardSimulationIterator, 154 | }; 155 | 156 | use super::*; 157 | 158 | #[test] 159 | fn test_standard_simulation() { 160 | let agent_gens: Vec> = vec![ 161 | Box::::default(), 162 | Box::::default(), 163 | ]; 164 | 165 | let stacks = vec![100.0; 2]; 166 | let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 167 | let sim_gen = StandardSimulationIterator::new( 168 | agent_gens, 169 | vec![], // no historians 170 | CloneGameStateGenerator::new(game_state), 171 | ); 172 | let mut competition = HoldemCompetition::new(sim_gen); 173 | 174 | let _first_results = competition.run(100).unwrap(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/simulated_icm/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module provides the ability to simulate a mutli table independent chip 2 | //! tournament. It does this via simulation. Different heros and villans go to 3 | //! all in show downs. Then the resulting placements are computed as each player 4 | //! busts. 5 | //! 6 | //! This method does not require a recursive dive N! so it makes simulating 7 | //! tournaments with many different people and different payments feasible. 8 | //! However it comes with some downsides. 9 | //! 10 | //! - The results are not repeatable. 11 | //! - Small SNG's would be faster to compute with full ICM rather than 12 | //! simulations 13 | //! 14 | //! However it does have some other nice properties 15 | //! 16 | //! - It's parrallelizable. This can be farmed out to many different cores to 17 | //! speed 18 | //! this up. Since each tournament is indepent there's little coordination 19 | //! oeverhead needed. 20 | //! - We can change the players skill easily. Since ICM just looks at the 21 | //! percentage or outstanding chips 22 | use rand::{Rng, rng, seq::SliceRandom}; 23 | 24 | /// Simulate a tournament by running a series of all 25 | /// in showdowns. This helps deterimine the value of each 26 | /// chip stack in a tournament with payout schedules. 27 | /// 28 | /// 29 | /// # Arguments 30 | /// 31 | /// * `chip_stacks` - The chip stacks of each player in the tournament. 32 | /// * `payments` - The payout schedule for the tournament. 33 | pub fn simulate_icm_tournament(chip_stacks: &[i32], payments: &[i32]) -> Vec { 34 | // We're going to mutate in place so move the chip stacks into a mutable vector. 35 | let mut remaining_stacks: Vec = chip_stacks.into(); 36 | // Thread local rng. 37 | let mut rng = rng(); 38 | // Which place in the next player to bust will get. 39 | let mut next_place = remaining_stacks.len() - 1; 40 | 41 | // The results. 42 | let mut winnings = vec![0; remaining_stacks.len()]; 43 | // set all the players as still having chips remaining. 44 | let mut remaining_players: Vec = (0..chip_stacks.len()).collect(); 45 | 46 | while !remaining_players.is_empty() { 47 | // Shuffle the players because we are going to use 48 | // the last two in the vector. 49 | // That allows O(1) pop and then usually push 50 | remaining_players.shuffle(&mut rng); 51 | 52 | // While this looks like it should be a ton of 53 | // mallocing and free-ing memory 54 | // because the vector never grows and ususally stays 55 | // the same size, it's remarkably fast. 56 | let hero = remaining_players.pop().expect("There should always be one"); 57 | 58 | // If there are two players remaining then run the game 59 | if let Some(villan) = remaining_players.pop() { 60 | // For now assume that each each player has the same skill. 61 | // TODO: Check to see if adding in a skill(running avg of win %) array for each 62 | // player is needed. 63 | let hero_won: bool = rng.random_bool(0.5); 64 | 65 | // can't bet chips that can't be called. 66 | let effective_stacks = remaining_stacks[hero].min(remaining_stacks[villan]); 67 | let hero_change: i32 = if hero_won { 68 | effective_stacks 69 | } else { 70 | -effective_stacks 71 | }; 72 | remaining_stacks[hero] += hero_change; 73 | remaining_stacks[villan] -= hero_change; 74 | 75 | // Check if hero was eliminated. 76 | if remaining_stacks[hero] == 0 { 77 | if next_place < payments.len() { 78 | winnings[hero] = payments[next_place]; 79 | } 80 | next_place -= 1; 81 | } else { 82 | remaining_players.push(hero); 83 | } 84 | 85 | // Now check if the villan was eliminated. 86 | if remaining_stacks[villan] == 0 { 87 | if next_place < payments.len() { 88 | winnings[villan] = payments[next_place]; 89 | } 90 | next_place -= 1; 91 | } else { 92 | remaining_players.push(villan); 93 | } 94 | } else { 95 | // If there's only a hero and no 96 | // villan then give the hero the money 97 | // 98 | // They have earned it. 99 | winnings[hero] = payments[next_place]; 100 | }; 101 | } 102 | winnings 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use super::*; 108 | 109 | #[test] 110 | fn test_num_players_works() { 111 | let payments = vec![10_000, 6_000, 4_000, 1_000, 800]; 112 | let mut rng = rng(); 113 | 114 | for num_players in &[2, 3, 4, 5, 15, 16, 32] { 115 | let chips: Vec = (0..*num_players) 116 | .map(|_pn| rng.random_range(1..500)) 117 | .collect(); 118 | 119 | let _res = simulate_icm_tournament(&chips, &payments); 120 | } 121 | } 122 | #[test] 123 | fn test_huge_lead_wins() { 124 | let stacks = vec![1000, 2, 1]; 125 | let payments = vec![100, 30, 10]; 126 | 127 | let mut total_winnings = vec![0; 3]; 128 | let num_trials = 1000; 129 | 130 | for _i in 0..num_trials { 131 | let single_wins = simulate_icm_tournament(&stacks, &payments); 132 | total_winnings = total_winnings 133 | .iter() 134 | .zip(single_wins.iter()) 135 | .map(|(a, b)| a + b) 136 | .collect() 137 | } 138 | 139 | let final_share: Vec = total_winnings 140 | .iter() 141 | .map(|v| f64::from(*v) / f64::from(num_trials)) 142 | .collect(); 143 | 144 | assert!( 145 | final_share[0] > final_share[1], 146 | "The total winnings of a player with most of the chips should be above the rest." 147 | ); 148 | } 149 | 150 | #[test] 151 | fn about_same() { 152 | let stacks = vec![1000, 1000, 999]; 153 | let payments = vec![100, 30, 10]; 154 | 155 | let mut total_winnings = vec![0; 3]; 156 | let num_trials = 1000; 157 | 158 | for _i in 0..num_trials { 159 | let single_wins = simulate_icm_tournament(&stacks, &payments); 160 | total_winnings = total_winnings 161 | .iter() 162 | .zip(single_wins.iter()) 163 | .map(|(a, b)| a + b) 164 | .collect(); 165 | } 166 | 167 | let final_share: Vec = total_winnings 168 | .iter() 169 | .map(|v| f64::from(*v) / f64::from(num_trials)) 170 | .collect(); 171 | 172 | let sum: f64 = final_share.iter().sum(); 173 | let avg = sum / (final_share.len() as f64); 174 | 175 | for &share in final_share.iter() { 176 | assert!(share < 1.1 * avg); 177 | assert!(1.1 * share > avg); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/arena/cfr/historian.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::action::Action; 2 | use crate::arena::game_state::Round; 3 | 4 | use crate::arena::action::AgentAction; 5 | 6 | use crate::arena::Historian; 7 | use crate::core::Card; 8 | 9 | use crate::arena::GameState; 10 | 11 | use crate::arena::HistorianError; 12 | 13 | use super::ActionGenerator; 14 | use super::CFRState; 15 | use super::NodeData; 16 | use super::PlayerData; 17 | use super::TerminalData; 18 | use super::TraversalState; 19 | 20 | /// The `CFRHistorian` struct is responsible for managing the state and actions 21 | /// within the Counterfactual Regret Minimization (CFR) algorithm for poker 22 | /// games. 23 | /// 24 | /// # Type Parameters 25 | /// - `T`: A type that implements the `ActionGenerator` trait, used to generate 26 | /// actions based on the current game state. 27 | /// 28 | /// # Fields 29 | /// - `traversal_state`: The current state of the traversal within the game 30 | /// tree. 31 | /// - `cfr_state`: The current state of the CFR algorithm, including node data 32 | /// and counts. 33 | /// - `action_generator`: An instance of the action generator used to map 34 | /// actions to indices. 35 | /// 36 | /// # Trait Implementations 37 | /// - `Historian`: Implements the `Historian` trait, allowing the `CFRHistorian` 38 | /// to record various game actions and states. 39 | pub struct CFRHistorian 40 | where 41 | T: ActionGenerator, 42 | { 43 | pub traversal_state: TraversalState, 44 | pub cfr_state: CFRState, 45 | pub action_generator: T, 46 | } 47 | 48 | impl CFRHistorian 49 | where 50 | T: ActionGenerator, 51 | { 52 | pub(crate) fn new(traversal_state: TraversalState, cfr_state: CFRState) -> Self { 53 | let action_generator = T::new(cfr_state.clone(), traversal_state.clone()); 54 | CFRHistorian { 55 | traversal_state, 56 | cfr_state, 57 | action_generator, 58 | } 59 | } 60 | 61 | /// Prepare to navigate to a child node. This will increment the count of 62 | /// the node we are coming from and return the index of the child node 63 | /// we are navigating to. 64 | pub(crate) fn ensure_target_node( 65 | &mut self, 66 | node_data: NodeData, 67 | ) -> Result { 68 | let from_node_idx = self.traversal_state.node_idx(); 69 | let from_child_idx = self.traversal_state.chosen_child_idx(); 70 | 71 | // Increment the count of the node we are coming from 72 | self.cfr_state 73 | .increment_count(from_node_idx, from_child_idx) 74 | .map_err(|_| HistorianError::CFRNodeNotFound)?; 75 | 76 | let to = self.cfr_state.get_child(from_node_idx, from_child_idx); 77 | 78 | match to { 79 | // The node already exists so our work is done here 80 | Some(t) => Ok(t), 81 | // The node doesn't exist so we need to create it with the provided data 82 | // 83 | // We then wrap it in an Ok so we tell the world how error free we are.... 84 | None => Ok(self.cfr_state.add(from_node_idx, from_child_idx, node_data)), 85 | } 86 | } 87 | 88 | pub(crate) fn record_card( 89 | &mut self, 90 | _game_state: &GameState, 91 | card: Card, 92 | ) -> Result<(), HistorianError> { 93 | let card_value: u8 = card.into(); 94 | let to_node_idx = self.ensure_target_node(NodeData::Chance)?; 95 | self.traversal_state 96 | .move_to(to_node_idx, card_value as usize); 97 | 98 | Ok(()) 99 | } 100 | 101 | pub(crate) fn record_action( 102 | &mut self, 103 | game_state: &GameState, 104 | action: AgentAction, 105 | player_idx: usize, 106 | ) -> Result<(), HistorianError> { 107 | let action_idx = self.action_generator.action_to_idx(game_state, &action); 108 | let to_node_idx = self.ensure_target_node(NodeData::Player(PlayerData { 109 | regret_matcher: Option::default(), 110 | player_idx, 111 | }))?; 112 | self.traversal_state.move_to(to_node_idx, action_idx); 113 | Ok(()) 114 | } 115 | 116 | pub(crate) fn record_terminal(&mut self, game_state: &GameState) -> Result<(), HistorianError> { 117 | let to_node_idx = self.ensure_target_node(NodeData::Terminal(TerminalData::default()))?; 118 | self.traversal_state.move_to(to_node_idx, 0); 119 | 120 | let reward = game_state.player_reward(self.traversal_state.player_idx()); 121 | 122 | // For terminal nodes we will never have a child so we repurpose 123 | // the child visited counter. 124 | self.cfr_state 125 | .increment_count(to_node_idx, 0) 126 | .map_err(|_| HistorianError::CFRNodeNotFound)?; 127 | 128 | self.cfr_state 129 | .update_node(to_node_idx, |node| { 130 | if let NodeData::Terminal(td) = &mut node.data { 131 | td.total_utility += reward; 132 | } 133 | }) 134 | .map_err(|_| HistorianError::CFRNodeNotFound)?; 135 | 136 | // Verify the node is actually a terminal node 137 | let node_data = self 138 | .cfr_state 139 | .get_node_data(to_node_idx) 140 | .ok_or(HistorianError::CFRNodeNotFound)?; 141 | 142 | if !matches!(node_data, NodeData::Terminal(_)) { 143 | return Err(HistorianError::CFRUnexpectedNode( 144 | "Expected terminal node".to_string(), 145 | )); 146 | } 147 | 148 | Ok(()) 149 | } 150 | } 151 | 152 | impl Historian for CFRHistorian 153 | where 154 | T: ActionGenerator, 155 | { 156 | fn record_action( 157 | &mut self, 158 | _id: u128, 159 | game_state: &GameState, 160 | action: Action, 161 | ) -> Result<(), HistorianError> { 162 | match action { 163 | // These are all assumed from game start and encoded in the root node. 164 | Action::GameStart(_) | Action::ForcedBet(_) | Action::PlayerSit(_) => Ok(()), 165 | // For the final round we need to use that to get the final award amount 166 | Action::RoundAdvance(Round::Complete) => self.record_terminal(game_state), 167 | // We don't encode round advance in the tree because it never changes the outcome. 168 | Action::RoundAdvance(_) => Ok(()), 169 | // Rather than use award since it can be for a side pot we use the final award ammount 170 | // in the terminal node. 171 | Action::Award(_) => Ok(()), 172 | Action::DealStartingHand(payload) => { 173 | // We only record our own hand 174 | // so the state can be shared between simulation runs. 175 | if payload.idx == self.traversal_state.player_idx() { 176 | self.record_card(game_state, payload.card) 177 | } else { 178 | Ok(()) 179 | } 180 | } 181 | Action::PlayedAction(payload) => { 182 | self.record_action(game_state, payload.action, payload.idx) 183 | } 184 | Action::FailedAction(failed_action_payload) => self.record_action( 185 | game_state, 186 | failed_action_payload.result.action, 187 | failed_action_payload.result.idx, 188 | ), 189 | Action::DealCommunity(card) => self.record_card(game_state, card), 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/arena/cfr/state_store.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, RwLock}; 2 | 3 | use crate::arena::GameState; 4 | 5 | use super::{CFRState, TraversalState}; 6 | 7 | #[derive(Debug, Clone)] 8 | struct StateStoreInternal { 9 | // The tree structure of counter factual regret. 10 | pub cfr_states: Vec, 11 | 12 | // The current place in the tree that each player is at. This is used as a stack 13 | pub traversal_states: Vec>, 14 | } 15 | 16 | /// `StateStore` is a structure to hold all CFR states and other data needed for 17 | /// a single game that is being solved. Since all players use the same store it 18 | /// enables reuse of the memory and regret matchers of all players. 19 | /// 20 | /// This state store is not thread safe so it has to be used in a single thread. 21 | #[derive(Debug, Clone)] 22 | pub struct StateStore { 23 | inner: Arc>, 24 | } 25 | 26 | impl StateStore { 27 | pub fn new() -> Self { 28 | StateStore { 29 | inner: Arc::new(RwLock::new(StateStoreInternal { 30 | cfr_states: Vec::new(), 31 | traversal_states: Vec::new(), 32 | })), 33 | } 34 | } 35 | 36 | pub fn len(&self) -> usize { 37 | self.inner.read().unwrap().cfr_states.len() 38 | } 39 | 40 | pub fn is_empty(&self) -> bool { 41 | self.len() == 0 42 | } 43 | 44 | pub fn traversal_len(&self, player_idx: usize) -> usize { 45 | self.inner 46 | .read() 47 | .unwrap() 48 | .traversal_states 49 | .get(player_idx) 50 | .map_or(0, |traversal| traversal.len()) 51 | } 52 | 53 | pub fn peek_traversal(&self, player_idx: usize) -> Option { 54 | self.inner 55 | .read() 56 | .unwrap() 57 | .traversal_states 58 | .get(player_idx) 59 | .and_then(|traversal| traversal.last().cloned()) 60 | } 61 | 62 | pub fn new_state( 63 | &mut self, 64 | game_state: GameState, 65 | player_idx: usize, 66 | ) -> (CFRState, TraversalState) { 67 | let mut inner = self.inner.write().unwrap(); 68 | 69 | // Add the CFR State 70 | inner.cfr_states.push(CFRState::new(game_state)); 71 | 72 | // We want a root traversal state for the new player 73 | // This won't ever be changed. 74 | inner 75 | .traversal_states 76 | .push(vec![TraversalState::new_root(player_idx)]); 77 | 78 | let traversal_states = inner 79 | .traversal_states 80 | .get_mut(player_idx) 81 | .unwrap_or_else(|| panic!("Traversal state for player {player_idx} not found")); 82 | 83 | let last = traversal_states.last().expect("No traversal state found"); 84 | 85 | // Make a copy and put it in the stack 86 | let new_traversal_state = 87 | TraversalState::new(last.node_idx(), last.chosen_child_idx(), last.player_idx()); 88 | 89 | // Create a new traversal state based on the last one 90 | traversal_states.push(new_traversal_state.clone()); 91 | 92 | // Get a clone of the cfr state to give out. 93 | let state = inner 94 | .cfr_states 95 | .get(player_idx) 96 | .unwrap_or_else(|| panic!("State for player {player_idx} not found")) 97 | .clone(); 98 | 99 | (state, new_traversal_state) 100 | } 101 | 102 | pub fn push_traversal(&mut self, player_idx: usize) -> (CFRState, TraversalState) { 103 | let mut inner = self.inner.write().unwrap(); 104 | 105 | let traversal_states = inner 106 | .traversal_states 107 | .get_mut(player_idx) 108 | .unwrap_or_else(|| panic!("Traversal state for player {player_idx} not found")); 109 | 110 | let last = traversal_states.last().expect("No traversal state found"); 111 | 112 | // Make a copy and put it in the stack 113 | let new_traversal_state = 114 | TraversalState::new(last.node_idx(), last.chosen_child_idx(), last.player_idx()); 115 | 116 | // Create a new traversal state based on the last one 117 | traversal_states.push(new_traversal_state.clone()); 118 | 119 | let cfr_state = inner 120 | .cfr_states 121 | .get(player_idx) 122 | .unwrap_or_else(|| panic!("State for player {player_idx} not found")) 123 | .clone(); 124 | 125 | (cfr_state, new_traversal_state) 126 | } 127 | 128 | pub fn pop_traversal(&mut self, player_idx: usize) { 129 | let mut inner = self.inner.write().unwrap(); 130 | let traversal_states = inner 131 | .traversal_states 132 | .get_mut(player_idx) 133 | .expect("Traversal state for player not found"); 134 | assert!( 135 | !traversal_states.is_empty(), 136 | "No traversal state to pop for player {player_idx}" 137 | ); 138 | traversal_states.pop(); 139 | } 140 | } 141 | 142 | impl Default for StateStore { 143 | fn default() -> Self { 144 | Self::new() 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | 151 | use super::*; 152 | 153 | #[test] 154 | fn test_new() { 155 | let store = StateStore::new(); 156 | assert_eq!(store.len(), 0, "New state store should have no states"); 157 | } 158 | 159 | #[test] 160 | fn test_push() { 161 | let mut state_store = StateStore::new(); 162 | let game_state = GameState::new_starting(vec![100.0; 3], 10.0, 5.0, 0.0, 0); 163 | let (state, _traversal) = state_store.new_state(game_state.clone(), 0); 164 | assert_eq!( 165 | state_store.len(), 166 | 1, 167 | "State store should have one state after push" 168 | ); 169 | assert_eq!( 170 | state.starting_game_state(), 171 | game_state, 172 | "State should match the game state" 173 | ); 174 | } 175 | 176 | #[test] 177 | fn test_push_len() { 178 | let mut state_store = StateStore::new(); 179 | 180 | let game_state = GameState::new_starting(vec![100.0; 3], 10.0, 5.0, 0.0, 0); 181 | 182 | let _stores = (0..2) 183 | .map(|i| { 184 | let (state, traversal) = state_store.new_state(game_state.clone(), i); 185 | assert_eq!( 186 | state_store.len(), 187 | i + 1, 188 | "State store should have one state after push" 189 | ); 190 | (state, traversal) 191 | }) 192 | .collect::>(); 193 | 194 | assert_eq!(2, state_store.len(), "State store should have two states"); 195 | 196 | let mut store_clones = (0..2).map(|_| state_store.clone()).collect::>(); 197 | 198 | for (player_idx, cloned_state_store) in store_clones.iter_mut().enumerate() { 199 | assert_eq!( 200 | cloned_state_store.len(), 201 | 2, 202 | "Cloned state store should have two states" 203 | ); 204 | 205 | let (_, _) = cloned_state_store.push_traversal(player_idx); 206 | assert_eq!( 207 | cloned_state_store.len(), 208 | 2, 209 | "Cloned state store should still have two states" 210 | ); 211 | } 212 | 213 | for i in 0..2 { 214 | state_store.pop_traversal(i); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/core/hand.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{BitAnd, BitAndAssign}; 2 | 3 | use super::{Card, CardBitSet, CardBitSetIter, RSPokerError, Suit, Value}; 4 | 5 | #[derive(Debug, Clone, PartialEq, Copy)] 6 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 7 | #[cfg_attr(feature = "serde", serde(transparent))] 8 | pub struct Hand(CardBitSet); 9 | 10 | impl Hand { 11 | /// Create a new empty hand 12 | /// 13 | /// # Examples 14 | /// 15 | /// ``` 16 | /// use rs_poker::core::Hand; 17 | /// 18 | /// let hand = Hand::new(); 19 | /// 20 | /// assert!(hand.is_empty()); 21 | /// ``` 22 | pub fn new() -> Self { 23 | Self(CardBitSet::new()) 24 | } 25 | 26 | pub fn new_with_cards(cards: Vec) -> Self { 27 | let mut bitset = CardBitSet::new(); 28 | for card in cards { 29 | bitset.insert(card); 30 | } 31 | Self(bitset) 32 | } 33 | 34 | /// Given a card, is it in the current hand? 35 | /// 36 | /// # Examples 37 | /// 38 | /// ``` 39 | /// use rs_poker::core::{Card, Hand, Suit, Value}; 40 | /// 41 | /// let mut hand = Hand::new(); 42 | /// 43 | /// let card = Card::new(Value::Ace, Suit::Club); 44 | /// assert!(!hand.contains(&card)); 45 | /// 46 | /// hand.insert(card); 47 | /// assert!(hand.contains(&card)); 48 | /// ``` 49 | pub fn contains(&self, c: &Card) -> bool { 50 | self.0.contains(*c) 51 | } 52 | 53 | /// Remove a card from the hand 54 | /// 55 | /// # Examples 56 | /// 57 | /// ``` 58 | /// use rs_poker::core::{Card, Hand, Suit, Value}; 59 | /// 60 | /// let mut hand = Hand::new(); 61 | /// 62 | /// let card = Card::new(Value::Ace, Suit::Club); 63 | /// assert!(!hand.contains(&card)); 64 | /// 65 | /// hand.insert(card); 66 | /// assert!(hand.contains(&card)); 67 | /// 68 | /// hand.remove(&card); 69 | /// assert!(!hand.contains(&card)); 70 | /// ``` 71 | pub fn remove(&mut self, c: &Card) -> bool { 72 | let contains = self.contains(c); 73 | self.0.remove(*c); 74 | contains 75 | } 76 | 77 | pub fn insert(&mut self, c: Card) -> bool { 78 | let contains = self.contains(&c); 79 | self.0.insert(c); 80 | !contains 81 | } 82 | 83 | pub fn count(&self) -> usize { 84 | self.0.count() 85 | } 86 | 87 | pub fn is_empty(&self) -> bool { 88 | self.0.is_empty() 89 | } 90 | 91 | pub fn iter(&self) -> CardBitSetIter { 92 | self.0.into_iter() 93 | } 94 | 95 | pub fn clear(&mut self) { 96 | self.0.clear(); 97 | } 98 | 99 | pub fn new_from_str(hand_string: &str) -> Result { 100 | let mut chars = hand_string.chars(); 101 | let mut bitset = CardBitSet::new(); 102 | 103 | // Keep looping until we explicitly break 104 | loop { 105 | let vco = chars.next(); 106 | if vco.is_none() { 107 | break; 108 | } else { 109 | let sco = chars.next(); 110 | let v = vco 111 | .and_then(Value::from_char) 112 | .ok_or(RSPokerError::UnexpectedValueChar)?; 113 | let s = sco 114 | .and_then(Suit::from_char) 115 | .ok_or(RSPokerError::UnexpectedSuitChar)?; 116 | 117 | let c = Card { value: v, suit: s }; 118 | 119 | if bitset.contains(c) { 120 | return Err(RSPokerError::DuplicateCardInHand(c)); 121 | } else { 122 | bitset.insert(c); 123 | } 124 | } 125 | } 126 | 127 | if chars.next().is_some() { 128 | return Err(RSPokerError::UnparsedCharsRemaining); 129 | } 130 | 131 | Ok(Self(bitset)) 132 | } 133 | } 134 | 135 | impl Default for Hand { 136 | fn default() -> Self { 137 | Self(CardBitSet::new()) 138 | } 139 | } 140 | 141 | impl Extend for Hand { 142 | fn extend>(&mut self, iter: T) { 143 | for card in iter { 144 | self.insert(card); 145 | } 146 | } 147 | } 148 | 149 | impl BitAnd for Hand { 150 | type Output = Hand; 151 | 152 | fn bitand(self, rhs: Self) -> Self::Output { 153 | Self(self.0 & rhs.0) 154 | } 155 | } 156 | 157 | impl BitAndAssign for Hand { 158 | fn bitand_assign(&mut self, rhs: Self) { 159 | self.0 &= rhs.0; 160 | } 161 | } 162 | 163 | impl From for CardBitSet { 164 | fn from(val: Hand) -> Self { 165 | val.0 166 | } 167 | } 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use super::*; 172 | 173 | #[test] 174 | fn test_insert() { 175 | let mut hand = Hand::new(); 176 | for i in 1..7 { 177 | let c = Card::from(i); 178 | assert!(hand.insert(c)); 179 | assert!(hand.contains(&c)); 180 | assert_eq!(hand.count(), usize::from(i)); 181 | } 182 | } 183 | 184 | #[test] 185 | fn test_is_empty() { 186 | let mut hand = Hand::new(); 187 | assert!(hand.is_empty()); 188 | 189 | hand.insert(Card::from(1)); 190 | assert!(!hand.is_empty()); 191 | hand.clear(); 192 | 193 | assert!(hand.is_empty()); 194 | hand.insert(Card::from(2)); 195 | assert!(!hand.is_empty()); 196 | } 197 | 198 | #[test] 199 | fn test_bit_and() { 200 | let mut hand1 = Hand::new(); 201 | let mut hand2 = Hand::new(); 202 | 203 | for i in 1..7 { 204 | let c = Card::from(i); 205 | hand1.insert(c); 206 | } 207 | 208 | for i in 4..10 { 209 | let c = Card::from(i); 210 | hand2.insert(c); 211 | } 212 | let hand3 = hand1 & hand2; 213 | assert_eq!(hand3.count(), 3); 214 | for i in 4..7 { 215 | let c = Card::from(i); 216 | assert!(hand3.contains(&c)); 217 | } 218 | for i in 1..4 { 219 | let c = Card::from(i); 220 | assert!(!hand3.contains(&c)); 221 | } 222 | } 223 | 224 | #[test] 225 | fn test_bit_and_assign() { 226 | let mut hand1 = Hand::new(); 227 | let mut hand2 = Hand::new(); 228 | 229 | for i in 1..7 { 230 | let c = Card::from(i); 231 | hand1.insert(c); 232 | } 233 | 234 | for i in 4..10 { 235 | let c = Card::from(i); 236 | hand2.insert(c); 237 | } 238 | hand1 &= hand2; 239 | assert_eq!(hand1.count(), 3); 240 | for i in 4..7 { 241 | let c = Card::from(i); 242 | assert!(hand1.contains(&c)); 243 | } 244 | for i in 1..4 { 245 | let c = Card::from(i); 246 | assert!(!hand1.contains(&c)); 247 | } 248 | } 249 | 250 | #[test] 251 | fn test_remove_return() { 252 | let mut hand = Hand::new(); 253 | hand.insert(Card::from(1)); 254 | hand.insert(Card::from(2)); 255 | 256 | assert_eq!(hand.count(), 2); 257 | 258 | assert!(hand.remove(&Card::from(1))); 259 | // It's already removed so this should return false 260 | assert!(!hand.remove(&Card::from(1))); 261 | 262 | // Now remove the other card 263 | assert!(hand.remove(&Card::from(2))); 264 | // I already removed it so this should return false 265 | assert!(!hand.remove(&Card::from(2))); 266 | 267 | assert!(hand.is_empty()); 268 | assert_eq!(hand.count(), 0); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/core/deck.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | 3 | use crate::core::card::Card; 4 | 5 | use super::{CardBitSet, CardBitSetIter}; 6 | 7 | /// Deck struct that can tell quickly if a card is in the deck 8 | /// 9 | /// # Examples 10 | /// 11 | /// ``` 12 | /// use rs_poker::core::{Card, Deck, Suit, Value}; 13 | /// 14 | /// // create a new deck 15 | /// let mut deck = Deck::new(); 16 | /// 17 | /// // add some cards to the deck 18 | /// deck.insert(Card::new(Value::Ace, Suit::Club)); 19 | /// deck.insert(Card::new(Value::King, Suit::Diamond)); 20 | /// deck.insert(Card::new(Value::Queen, Suit::Heart)); 21 | /// 22 | /// // check if a card is in the deck 23 | /// let card = Card::new(Value::Ace, Suit::Club); 24 | /// assert!(deck.contains(&card)); 25 | /// 26 | /// // remove a card from the deck 27 | /// assert!(deck.remove(&card)); 28 | /// assert!(!deck.contains(&card)); 29 | /// 30 | /// // get the number of cards in the deck 31 | /// assert_eq!(deck.len(), 2); 32 | /// 33 | /// // check if the deck is empty 34 | /// assert!(!deck.is_empty()); 35 | /// 36 | /// // get an iterator from the deck 37 | /// for card in deck.iter() { 38 | /// println!("{:?}", card); 39 | /// } 40 | /// ``` 41 | #[derive(Debug, Clone, Copy, PartialEq)] 42 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 43 | #[cfg_attr(feature = "serde", serde(transparent))] 44 | pub struct Deck(CardBitSet); 45 | 46 | impl Deck { 47 | /// Create a new empty deck 48 | /// 49 | /// # Examples 50 | /// 51 | /// ``` 52 | /// use rs_poker::core::Deck; 53 | /// 54 | /// let deck = Deck::new(); 55 | /// 56 | /// assert!(deck.is_empty()); 57 | /// assert_eq!(0, deck.len()); 58 | /// ``` 59 | pub fn new() -> Self { 60 | Self(CardBitSet::new()) 61 | } 62 | /// Given a card, is it in the current deck? 63 | pub fn contains(&self, c: &Card) -> bool { 64 | self.0.contains(*c) 65 | } 66 | /// Given a card remove it from the deck if it is present. 67 | pub fn remove(&mut self, c: &Card) -> bool { 68 | let contains = self.contains(c); 69 | self.0.remove(*c); 70 | contains 71 | } 72 | /// Add a given card to the deck. 73 | pub fn insert(&mut self, c: Card) -> bool { 74 | let contains = self.contains(&c); 75 | self.0.insert(c); 76 | !contains 77 | } 78 | /// How many cards are there in the deck. 79 | pub fn count(&self) -> usize { 80 | self.0.count() 81 | } 82 | /// Have all of the cards been dealt from this deck? 83 | pub fn is_empty(&self) -> bool { 84 | self.0.is_empty() 85 | } 86 | /// Get an iterator from this deck 87 | pub fn iter(&self) -> CardBitSetIter { 88 | self.0.into_iter() 89 | } 90 | 91 | pub fn len(&self) -> usize { 92 | self.0.count() 93 | } 94 | 95 | pub fn deal(&mut self, rng: &mut R) -> Option { 96 | let card = self.0.sample_one(rng); 97 | if let Some(c) = card { 98 | // remove the card from the deck 99 | self.remove(&c); 100 | Some(c) 101 | } else { 102 | None 103 | } 104 | } 105 | } 106 | 107 | /// Turn a deck into an iterator 108 | impl IntoIterator for Deck { 109 | type Item = Card; 110 | type IntoIter = CardBitSetIter; 111 | /// Consume this deck and create a new iterator. 112 | fn into_iter(self) -> CardBitSetIter { 113 | self.0.into_iter() 114 | } 115 | } 116 | 117 | impl Default for Deck { 118 | /// Create the default 52 card deck 119 | /// 120 | /// ``` 121 | /// use rs_poker::core::Deck; 122 | /// 123 | /// assert_eq!(52, Deck::default().len()); 124 | /// ``` 125 | fn default() -> Self { 126 | Self(CardBitSet::default()) 127 | } 128 | } 129 | 130 | impl From for Deck { 131 | /// Convert a `CardBitSet` into a `Deck`. 132 | /// 133 | /// # Examples 134 | /// 135 | /// ``` 136 | /// use rs_poker::core::CardBitSet; 137 | /// use rs_poker::core::{Card, Deck, Suit, Value}; 138 | /// 139 | /// let mut card_bit_set = CardBitSet::new(); 140 | /// 141 | /// // Add some cards to the CardBitSet 142 | /// 143 | /// card_bit_set.insert(Card::new(Value::Ace, Suit::Club)); 144 | /// card_bit_set.insert(Card::new(Value::King, Suit::Diamond)); 145 | /// 146 | /// // Convert the CardBitSet into a Deck 147 | /// let deck: Deck = card_bit_set.into(); 148 | /// 149 | /// assert_eq!(2, deck.len()); 150 | /// assert!(deck.contains(&Card::new(Value::Ace, Suit::Club))); 151 | /// assert!(deck.contains(&Card::new(Value::King, Suit::Diamond))); 152 | /// ``` 153 | fn from(val: CardBitSet) -> Self { 154 | Deck(val) 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use rand::{SeedableRng, rngs::StdRng}; 161 | 162 | use crate::core::{Suit, Value}; 163 | 164 | use super::*; 165 | 166 | #[test] 167 | fn test_contains_in() { 168 | let d = Deck::default(); 169 | assert!(d.contains(&Card { 170 | value: Value::Eight, 171 | suit: Suit::Heart, 172 | })); 173 | } 174 | 175 | #[test] 176 | fn test_remove() { 177 | let mut d = Deck::default(); 178 | let c = Card { 179 | value: Value::Ace, 180 | suit: Suit::Heart, 181 | }; 182 | assert!(d.contains(&c)); 183 | assert!(d.remove(&c)); 184 | assert!(!d.contains(&c)); 185 | assert!(!d.remove(&c)); 186 | } 187 | 188 | #[test] 189 | fn test_deal() { 190 | let mut d = Deck::default(); 191 | let mut rng = rand::rng(); 192 | let c = d.deal(&mut rng); 193 | assert!(c.is_some()); 194 | assert!(!d.contains(&c.unwrap())); 195 | 196 | let other = d.deal(&mut rng); 197 | assert!(other.is_some()); 198 | 199 | assert_ne!(c, other); 200 | assert_eq!(d.len(), 50); 201 | } 202 | 203 | #[test] 204 | fn test_deal_all() { 205 | let mut cards_dealt = 0; 206 | let mut d = Deck::default(); 207 | 208 | let mut rng = rand::rng(); 209 | 210 | while let Some(_c) = d.deal(&mut rng) { 211 | cards_dealt += 1; 212 | } 213 | assert_eq!(cards_dealt, 52); 214 | assert!(d.is_empty()); 215 | } 216 | 217 | #[test] 218 | fn test_stable_deal_order_with_seed_rng() { 219 | let mut rng_one = StdRng::seed_from_u64(420); 220 | let mut rng_two = StdRng::seed_from_u64(420); 221 | 222 | let mut d_one = Deck::default(); 223 | let mut d_two = Deck::default(); 224 | 225 | let mut cards_dealt_one = Vec::with_capacity(52); 226 | let mut cards_dealt_two = Vec::with_capacity(52); 227 | 228 | while let Some(c) = d_one.deal(&mut rng_one) { 229 | cards_dealt_one.push(c); 230 | } 231 | while let Some(c) = d_two.deal(&mut rng_two) { 232 | cards_dealt_two.push(c); 233 | } 234 | assert_eq!(cards_dealt_one, cards_dealt_two); 235 | assert!(d_one.is_empty()); 236 | assert!(d_two.is_empty()); 237 | } 238 | 239 | #[test] 240 | fn test_insert_returns_bool() { 241 | let mut d = Deck::new(); 242 | let c = Card { 243 | value: Value::Ace, 244 | suit: Suit::Heart, 245 | }; 246 | assert!(d.insert(c)); 247 | assert!(!d.insert(c)); 248 | assert!(d.contains(&c)); 249 | assert_eq!(d.len(), 1); 250 | 251 | let c2 = Card { 252 | value: Value::Two, 253 | suit: Suit::Heart, 254 | }; 255 | assert!(d.insert(c2)); 256 | assert!(!d.insert(c2)); 257 | assert!(d.contains(&c2)); 258 | assert_eq!(d.len(), 2); 259 | } 260 | 261 | #[test] 262 | fn test_count_zero() { 263 | let d = Deck::new(); 264 | assert_eq!(0, d.count()); 265 | } 266 | 267 | #[test] 268 | fn test_count_after_adding() { 269 | let mut d = Deck::new(); 270 | 271 | let c = Card { 272 | value: Value::Ace, 273 | suit: Suit::Heart, 274 | }; 275 | 276 | d.insert(c); 277 | assert_eq!(1, d.count()); 278 | d.insert(Card { 279 | value: Value::Two, 280 | suit: Suit::Heart, 281 | }); 282 | 283 | assert_eq!(2, d.count()); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/arena/cfr/action_generator.rs: -------------------------------------------------------------------------------- 1 | use tracing::event; 2 | 3 | use crate::arena::{GameState, action::AgentAction}; 4 | 5 | use super::{CFRState, NodeData, TraversalState}; 6 | 7 | pub trait ActionGenerator { 8 | /// Create a new action generator 9 | /// 10 | /// This is used by the Agent to create identical 11 | /// action generators for the historians it uses. 12 | fn new(cfr_state: CFRState, traversal_state: TraversalState) -> Self; 13 | 14 | /// Given an action return the index of the action in the children array. 15 | /// 16 | /// # Arguments 17 | /// 18 | /// * `game_state` - The current game state 19 | /// * `action` - The action to convert to an index 20 | /// 21 | /// # Returns 22 | /// 23 | /// The index of the action in the children array. The 0 index is the fold 24 | /// action. All other are defined by the implentation 25 | fn action_to_idx(&self, game_state: &GameState, action: &AgentAction) -> usize; 26 | 27 | /// How many potential actions in total might be generated. 28 | /// 29 | /// At a given node there might be fewere that will be 30 | /// possible, but the regret matcher doesn't keep track of that. 31 | /// 32 | /// At all time the number of potential actions is 33 | /// larger than or equal to the number of possible actions 34 | /// 35 | /// # Returns 36 | /// 37 | /// The number of potential actions 38 | fn num_potential_actions(&self, game_state: &GameState) -> usize; 39 | 40 | // Generate all possible actions for the current game state 41 | // 42 | // This returns a vector so that the actions can be chosen from randomly 43 | fn gen_possible_actions(&self, game_state: &GameState) -> Vec; 44 | 45 | // Using the current and the CFR's tree's regret state choose a single action to 46 | // play. 47 | fn gen_action(&self, game_state: &GameState) -> AgentAction; 48 | } 49 | 50 | pub struct BasicCFRActionGenerator { 51 | cfr_state: CFRState, 52 | traversal_state: TraversalState, 53 | } 54 | 55 | impl BasicCFRActionGenerator { 56 | pub fn new(cfr_state: CFRState, traversal_state: TraversalState) -> Self { 57 | BasicCFRActionGenerator { 58 | cfr_state, 59 | traversal_state, 60 | } 61 | } 62 | } 63 | 64 | impl ActionGenerator for BasicCFRActionGenerator { 65 | fn gen_action(&self, game_state: &GameState) -> AgentAction { 66 | let possible = self.gen_possible_actions(game_state); 67 | // For now always use the thread rng. 68 | // At somepoint we will want to be able to pass seeded or deterministic action 69 | // choices. 70 | let mut rng = rand::rng(); 71 | 72 | // Get target node index 73 | let from_node_idx = self.traversal_state.node_idx(); 74 | let from_child_idx = self.traversal_state.chosen_child_idx(); 75 | let target_node_idx = self 76 | .cfr_state 77 | .get_child(from_node_idx, from_child_idx) 78 | .expect("Expected target node"); 79 | 80 | // Get the node data 81 | let node_data = self 82 | .cfr_state 83 | .get_node_data(target_node_idx) 84 | .expect("Expected target node data"); 85 | 86 | if let NodeData::Player(pd) = &node_data { 87 | let next_action = pd 88 | .regret_matcher 89 | .as_ref() 90 | .map_or(0, |matcher| matcher.next_action(&mut rng)); 91 | 92 | event!( 93 | tracing::Level::DEBUG, 94 | next_action = next_action, 95 | "Next action index" 96 | ); 97 | 98 | // Find the first action that matches the index picked from the regret matcher 99 | possible 100 | .iter() 101 | .find_map(|action| { 102 | if self.action_to_idx(game_state, action) == next_action { 103 | Some(action.clone()) 104 | } else { 105 | None 106 | } 107 | }) 108 | .unwrap_or_else(|| { 109 | // Just in case the regret matcher returns an action that is not in the possible actions 110 | // choose the first possible action as a fallback or fold if there are no possible actions 111 | let fallback = possible.first().unwrap_or(&AgentAction::Fold).clone(); 112 | event!(tracing::Level::WARN, fallback = ?fallback, "No action found for next action index"); 113 | fallback 114 | }) 115 | } else { 116 | panic!("Expected player node"); 117 | } 118 | } 119 | 120 | fn new(cfr_state: CFRState, traversal_state: TraversalState) -> Self { 121 | BasicCFRActionGenerator { 122 | cfr_state, 123 | traversal_state, 124 | } 125 | } 126 | 127 | fn gen_possible_actions(&self, game_state: &GameState) -> Vec { 128 | let mut res: Vec = Vec::with_capacity(3); 129 | let to_call = 130 | game_state.current_round_bet() - game_state.current_round_current_player_bet(); 131 | if to_call > 0.0 { 132 | res.push(AgentAction::Fold); 133 | } 134 | // Call, Match the current bet (if the bet is 0 this is a check) 135 | res.push(AgentAction::Bet(game_state.current_round_bet())); 136 | 137 | let all_in_ammount = 138 | game_state.current_round_current_player_bet() + game_state.current_player_stack(); 139 | 140 | if all_in_ammount > game_state.current_round_bet() { 141 | // All-in, Bet all the money 142 | // Bet everything we have bet so far plus the remaining stack 143 | res.push(AgentAction::AllIn); 144 | } 145 | res 146 | } 147 | 148 | fn action_to_idx(&self, _game_state: &GameState, action: &AgentAction) -> usize { 149 | match action { 150 | AgentAction::Fold => 0, 151 | AgentAction::Bet(_) => 1, 152 | AgentAction::AllIn => 2, 153 | _ => panic!("Unexpected action {action:?}"), 154 | } 155 | } 156 | 157 | fn num_potential_actions(&self, _game_state: &GameState) -> usize { 158 | 3 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | mod tests { 164 | use super::*; 165 | 166 | use crate::arena::GameState; 167 | 168 | use std::vec; 169 | 170 | #[test] 171 | fn test_should_gen_2_actions() { 172 | let stacks = vec![50.0; 2]; 173 | let game_state = GameState::new_starting(stacks, 2.0, 1.0, 0.0, 0); 174 | let action_generator = BasicCFRActionGenerator::new( 175 | CFRState::new(game_state.clone()), 176 | TraversalState::new_root(0), 177 | ); 178 | let actions = action_generator.gen_possible_actions(&game_state); 179 | // We should have 2 actions: Call or All-in since 0 is the dealer when starting 180 | assert_eq!(actions.len(), 2); 181 | 182 | // None of the ations should have a child idx of 0 183 | for action in actions { 184 | assert_ne!(action_generator.action_to_idx(&game_state, &action), 0); 185 | } 186 | } 187 | 188 | #[test] 189 | fn test_should_gen_3_actions() { 190 | let stacks = vec![50.0; 2]; 191 | let mut game_state = GameState::new_starting(stacks, 2.0, 1.0, 0.0, 0); 192 | game_state.advance_round(); 193 | game_state.advance_round(); 194 | 195 | game_state.do_bet(10.0, false).unwrap(); 196 | let action_generator = BasicCFRActionGenerator::new( 197 | CFRState::new(game_state.clone()), 198 | TraversalState::new_root(0), 199 | ); 200 | let actions = action_generator.gen_possible_actions(&game_state); 201 | // We should have 3 actions: Fold, Call, or All-in 202 | assert_eq!(actions.len(), 3); 203 | 204 | // Check the indices of the actions 205 | assert_eq!( 206 | action_generator.action_to_idx(&game_state, &AgentAction::Fold), 207 | 0 208 | ); 209 | assert_eq!( 210 | action_generator.action_to_idx(&game_state, &AgentAction::Bet(10.0)), 211 | 1 212 | ); 213 | assert_eq!( 214 | action_generator.action_to_idx(&game_state, &AgentAction::AllIn), 215 | 2 216 | ); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/arena/cfr/node.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct PlayerData { 3 | pub regret_matcher: Option>, 4 | pub player_idx: usize, 5 | } 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct TerminalData { 9 | pub total_utility: f32, 10 | } 11 | 12 | impl TerminalData { 13 | pub fn new(total_utility: f32) -> Self { 14 | TerminalData { total_utility } 15 | } 16 | } 17 | 18 | impl Default for TerminalData { 19 | fn default() -> Self { 20 | TerminalData::new(0.0) 21 | } 22 | } 23 | 24 | // The base node type for Poker CFR 25 | #[derive(Debug, Clone)] 26 | pub enum NodeData { 27 | /// The root node. 28 | /// 29 | /// This node is always the first node in the tree, we don't 30 | /// use the GameStart action to create the node. By egarly 31 | /// creating the root node we can simplify the traversal. 32 | /// All that's required is to ignore GameStart, ForcedBet, and 33 | /// PlayerSit actions as they are all assumed in the root node. 34 | /// 35 | /// For all traversals we start at the root node and then follow the 36 | /// 0th child node for the first real action that follows from 37 | /// the starting game state. That could be a chance card if the player 38 | /// is going to get dealt starting hands, or it could be the first 39 | /// player action if the gamestate starts with hands already dealt. 40 | Root, 41 | 42 | /// A chance node. 43 | /// 44 | /// This node represents the dealing of a single card. 45 | /// Each child index in the children array represents a card. 46 | /// The count array is used to track the number of times a card 47 | /// has been dealt. 48 | Chance, 49 | Player(PlayerData), 50 | Terminal(TerminalData), 51 | } 52 | 53 | impl NodeData { 54 | pub fn is_terminal(&self) -> bool { 55 | matches!(self, NodeData::Terminal(_)) 56 | } 57 | 58 | pub fn is_chance(&self) -> bool { 59 | matches!(self, NodeData::Chance) 60 | } 61 | 62 | pub fn is_player(&self) -> bool { 63 | matches!(self, NodeData::Player(_)) 64 | } 65 | 66 | pub fn is_root(&self) -> bool { 67 | matches!(self, NodeData::Root) 68 | } 69 | } 70 | 71 | impl std::fmt::Display for NodeData { 72 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 73 | match self { 74 | NodeData::Root => write!(f, "Root"), 75 | NodeData::Chance => write!(f, "Chance"), 76 | NodeData::Player(_) => write!(f, "Player"), 77 | NodeData::Terminal(_) => write!(f, "Terminal"), 78 | } 79 | } 80 | } 81 | 82 | #[derive(Debug, Clone)] 83 | pub struct Node { 84 | pub idx: usize, 85 | pub data: NodeData, 86 | pub parent: Option, 87 | pub parent_child_idx: Option, 88 | 89 | // We use an array of Option to represent the children of the node. 90 | // The index of the array is the action index or the card index for chance nodes. 91 | // 92 | // This limits the number of possible agent actions to 52, but in return we 93 | // get contiguous memory for no pointer chasing. 94 | children: [Option; 52], 95 | count: [u32; 52], 96 | } 97 | 98 | impl Node { 99 | pub fn new_root() -> Self { 100 | Node { 101 | idx: 0, 102 | data: NodeData::Root, 103 | parent: Some(0), 104 | parent_child_idx: None, 105 | children: [None; 52], 106 | count: [0; 52], 107 | } 108 | } 109 | 110 | /// Create a new node with the provided index, parent index, and data. 111 | /// 112 | /// # Arguments 113 | /// 114 | /// * `idx` - The index of the node 115 | /// * `parent` - The index of the parent node 116 | /// * `data` - The data for the node 117 | /// 118 | /// # Returns 119 | /// 120 | /// A new node with the provided index, parent index, and data. 121 | /// 122 | /// # Example 123 | /// 124 | /// ``` 125 | /// use rs_poker::arena::cfr::{Node, NodeData}; 126 | /// 127 | /// let idx = 1; 128 | /// let parent = 0; 129 | /// let parent_child_idx = 0; 130 | /// let data = NodeData::Chance; 131 | /// let node = Node::new(idx, parent, parent_child_idx, data); 132 | /// ``` 133 | pub fn new(idx: usize, parent: usize, parent_child_idx: usize, data: NodeData) -> Self { 134 | Node { 135 | idx, 136 | data, 137 | parent: Some(parent), 138 | parent_child_idx: Some(parent_child_idx), 139 | children: [None; 52], 140 | count: [0; 52], 141 | } 142 | } 143 | 144 | // Set child node at the provided index 145 | pub fn set_child(&mut self, idx: usize, child: usize) { 146 | assert_eq!(self.children[idx], None); 147 | self.children[idx] = Some(child); 148 | } 149 | 150 | // Get the child node at the provided index 151 | pub fn get_child(&self, idx: usize) -> Option { 152 | self.children[idx] 153 | } 154 | 155 | // Increment the count for the provided index 156 | pub fn increment_count(&mut self, idx: usize) { 157 | assert!(idx == 0 || !self.data.is_terminal()); 158 | self.count[idx] += 1; 159 | } 160 | 161 | /// Get an iterator over all the node's children with their indices 162 | /// 163 | /// This is useful for traversing the tree for visualization or debugging. 164 | /// 165 | /// # Returns 166 | /// 167 | /// An iterator over tuples of (child_idx, child_node_idx) where: 168 | /// - child_idx is the index in the children array 169 | /// - child_node_idx is the index of the child node in the nodes vector 170 | pub fn iter_children(&self) -> impl Iterator + '_ { 171 | self.children 172 | .iter() 173 | .enumerate() 174 | .filter_map(|(idx, &child)| child.map(|c| (idx, c))) 175 | } 176 | 177 | /// Get the count for a specific child index 178 | /// 179 | /// # Arguments 180 | /// 181 | /// * `idx` - The index of the child 182 | /// 183 | /// # Returns 184 | /// 185 | /// The count for the specified child 186 | pub fn get_count(&self, idx: usize) -> u32 { 187 | self.count[idx] 188 | } 189 | } 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | use super::*; 194 | 195 | #[test] 196 | fn test_terminal_data_default() { 197 | let terminal_data = TerminalData::default(); 198 | assert_eq!(terminal_data.total_utility, 0.0); 199 | } 200 | 201 | #[test] 202 | fn test_terminal_data_new() { 203 | let terminal_data = TerminalData::new(10.0); 204 | assert_eq!(terminal_data.total_utility, 10.0); 205 | } 206 | 207 | #[test] 208 | fn test_node_data_is_terminal() { 209 | let node_data = NodeData::Terminal(TerminalData::new(10.0)); 210 | assert!(node_data.is_terminal()); 211 | } 212 | 213 | #[test] 214 | fn test_node_data_is_chance() { 215 | let node_data = NodeData::Chance; 216 | assert!(node_data.is_chance()); 217 | } 218 | 219 | #[test] 220 | fn test_node_data_is_player() { 221 | let node_data = NodeData::Player(PlayerData { 222 | regret_matcher: None, 223 | player_idx: 0, 224 | }); 225 | assert!(node_data.is_player()); 226 | } 227 | 228 | #[test] 229 | fn test_node_data_is_root() { 230 | let node_data = NodeData::Root; 231 | assert!(node_data.is_root()); 232 | } 233 | 234 | #[test] 235 | fn test_node_new_root() { 236 | let node = Node::new_root(); 237 | assert_eq!(node.idx, 0); 238 | // Root is it's own parent 239 | assert!(node.parent.is_some()); 240 | assert_eq!(node.parent, Some(0)); 241 | assert!(matches!(node.data, NodeData::Root)); 242 | } 243 | 244 | #[test] 245 | fn test_node_new() { 246 | let node = Node::new(1, 0, 0, NodeData::Chance); 247 | assert_eq!(node.idx, 1); 248 | assert_eq!(node.parent, Some(0)); 249 | assert!(matches!(node.data, NodeData::Chance)); 250 | } 251 | 252 | #[test] 253 | fn test_node_set_get_child() { 254 | let mut node = Node::new(1, 0, 0, NodeData::Chance); 255 | node.set_child(0, 2); 256 | assert_eq!(node.get_child(0), Some(2)); 257 | } 258 | 259 | #[test] 260 | fn test_node_increment_count() { 261 | let mut node = Node::new(1, 0, 0, NodeData::Chance); 262 | node.increment_count(0); 263 | assert_eq!(node.count[0], 1); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/core/player_bit_set.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{Debug, Display}, 3 | ops::{BitAnd, BitOr}, 4 | }; 5 | 6 | /// A struct representing a bit set for players. 7 | /// 8 | /// # Examples 9 | /// 10 | /// ``` 11 | /// use rs_poker::core::PlayerBitSet; 12 | /// let mut active_players = PlayerBitSet::new(9); 13 | /// 14 | /// // Player 4 folds 15 | /// active_players.disable(4); 16 | /// assert_eq!(8, active_players.count()); 17 | /// ``` 18 | #[derive(Default, Clone, Copy, PartialEq, Eq)] 19 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 20 | pub struct PlayerBitSet { 21 | set: u16, 22 | } 23 | 24 | impl std::hash::Hash for PlayerBitSet { 25 | fn hash(&self, state: &mut H) { 26 | self.set.hash(state); 27 | } 28 | } 29 | 30 | impl Debug for PlayerBitSet { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | write!(f, "PlayerBitSet[")?; 33 | 34 | for idx in 0..16 { 35 | if self.get(idx) { 36 | write!(f, "A")?; 37 | } else { 38 | write!(f, "_")?; 39 | } 40 | } 41 | 42 | write!(f, "]") 43 | } 44 | } 45 | 46 | impl Display for PlayerBitSet { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | write!(f, "[")?; 49 | 50 | for idx in 0..16 { 51 | if self.get(idx) { 52 | write!(f, "A")?; 53 | } else { 54 | write!(f, "_")?; 55 | } 56 | } 57 | 58 | write!(f, "]") 59 | } 60 | } 61 | 62 | impl PlayerBitSet { 63 | /// Creates a new `PlayerBitSet` with `players` number of players. 64 | pub fn new(players: usize) -> Self { 65 | let set = (1 << players) - 1; 66 | Self { set } 67 | } 68 | 69 | /// Returns the number of players enabled in the bit set. 70 | pub fn count(&self) -> usize { 71 | self.set.count_ones() as usize 72 | } 73 | 74 | /// Returns `true` if the bit set is empty. 75 | pub fn empty(&self) -> bool { 76 | self.set == 0 77 | } 78 | 79 | /// Enables the player at `idx` position in the bit set. 80 | pub fn enable(&mut self, idx: usize) { 81 | self.set |= 1 << idx; 82 | } 83 | 84 | /// Disables the player at `idx` position in the bit set. 85 | pub fn disable(&mut self, idx: usize) { 86 | self.set &= !(1 << idx); 87 | } 88 | 89 | /// Returns `true` if the player at `idx` position in the bit set is 90 | /// enabled. 91 | pub fn get(&self, idx: usize) -> bool { 92 | (self.set & (1 << idx)) != 0 93 | } 94 | 95 | /// Returns an iterator over the active players in the bit set. 96 | pub fn ones(self) -> ActivePlayerBitSetIter { 97 | ActivePlayerBitSetIter { set: self.set } 98 | } 99 | } 100 | 101 | /// Implements the BitOr trait for PlayerBitSet, allowing two PlayerBitSet 102 | /// instances to be combined using the | operator. 103 | /// 104 | /// # Examples 105 | /// 106 | /// ``` 107 | /// use rs_poker::core::PlayerBitSet; 108 | /// 109 | /// let mut a = PlayerBitSet::default(); 110 | /// a.enable(0); 111 | /// a.enable(2); 112 | /// 113 | /// let mut b = PlayerBitSet::default(); 114 | /// b.enable(1); 115 | /// b.enable(2); 116 | /// 117 | /// let c = a | b; 118 | /// assert_eq!(c.get(0), true); 119 | /// assert_eq!(c.get(1), true); 120 | /// assert_eq!(c.get(2), true); 121 | /// ``` 122 | /// 123 | /// Here, two `PlayerBitSet` instances `a` and `b` are combined using the `|` 124 | /// operator to produce a new `PlayerBitSet` instance `c`. `c` contains all the 125 | /// bits set in either `a` or `b`. 126 | impl BitOr for PlayerBitSet { 127 | type Output = PlayerBitSet; 128 | 129 | fn bitor(self, rhs: Self) -> Self::Output { 130 | Self { 131 | set: self.set | rhs.set, 132 | } 133 | } 134 | } 135 | 136 | impl BitAnd for PlayerBitSet { 137 | type Output = PlayerBitSet; 138 | 139 | fn bitand(self, rhs: Self) -> Self::Output { 140 | Self { 141 | set: self.set & rhs.set, 142 | } 143 | } 144 | } 145 | 146 | /// An iterator over the active players in a bit set. 147 | pub struct ActivePlayerBitSetIter { 148 | set: u16, 149 | } 150 | 151 | /// # Examples 152 | /// 153 | /// ``` 154 | /// use rs_poker::core::PlayerBitSet; 155 | /// 156 | /// let mut set = PlayerBitSet::default(); 157 | /// 158 | /// set.enable(0); 159 | /// set.enable(4); 160 | /// set.enable(2); 161 | /// 162 | /// let mut iter = set.ones(); 163 | /// 164 | /// assert_eq!(iter.next(), Some(0)); 165 | /// assert_eq!(iter.next(), Some(2)); 166 | /// assert_eq!(iter.next(), Some(4)); 167 | /// assert_eq!(iter.next(), None); 168 | /// ``` 169 | /// 170 | /// Here, we create a `PlayerBitSet` instance `set` and set some bits in it. We 171 | /// then create an `ActivePlayerBitSetIter` iterator from the `set` instance and 172 | /// use it to iterate over the active players. We verify that the iterator 173 | /// returns the correct indices of the active players. 174 | impl Iterator for ActivePlayerBitSetIter { 175 | type Item = usize; 176 | 177 | /// Returns the next active player in the bit set, or `None` if there are no 178 | /// more. 179 | fn next(&mut self) -> Option { 180 | if self.set == 0 { 181 | None 182 | } else { 183 | // Find the index of the first non-zero bit 184 | let idx = self.set.trailing_zeros() as usize; 185 | // Then set the first non-zero bit to zero 186 | self.set &= !(1 << idx); 187 | // Then emit the next active player 188 | Some(idx) 189 | } 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::*; 196 | 197 | #[test] 198 | fn test_new_count() { 199 | assert_eq!(7, PlayerBitSet::new(7).count()); 200 | } 201 | 202 | #[test] 203 | fn test_default_zero_count() { 204 | assert_eq!(0, PlayerBitSet::default().count()); 205 | } 206 | 207 | #[test] 208 | fn test_disable_count() { 209 | let mut s = PlayerBitSet::new(7); 210 | 211 | assert_eq!(7, s.count()); 212 | s.disable(6); 213 | assert_eq!(6, s.count()); 214 | s.disable(0); 215 | assert_eq!(5, s.count()); 216 | } 217 | 218 | #[test] 219 | fn test_enable_count() { 220 | let mut s = PlayerBitSet::default(); 221 | 222 | assert_eq!(0, s.count()); 223 | s.enable(0); 224 | assert_eq!(1, s.count()); 225 | s.enable(0); 226 | assert_eq!(1, s.count()); 227 | 228 | s.enable(2); 229 | assert_eq!(2, s.count()); 230 | 231 | s.disable(0); 232 | assert_eq!(1, s.count()); 233 | } 234 | 235 | #[test] 236 | fn test_iter() { 237 | let s = PlayerBitSet::new(2); 238 | let mut iter = s.ones(); 239 | 240 | assert_eq!(Some(0), iter.next()); 241 | assert_eq!(Some(1), iter.next()); 242 | assert_eq!(None, iter.next()); 243 | } 244 | 245 | #[test] 246 | fn test_iter_with_disabled() { 247 | let mut s = PlayerBitSet::new(3); 248 | let mut iter = s.ones(); 249 | 250 | assert_eq!(Some(0), iter.next()); 251 | assert_eq!(Some(1), iter.next()); 252 | assert_eq!(Some(2), iter.next()); 253 | assert_eq!(None, iter.next()); 254 | 255 | s.disable(0); 256 | 257 | let mut after_iter = s.ones(); 258 | assert_eq!(Some(1), after_iter.next()); 259 | assert_eq!(Some(2), after_iter.next()); 260 | assert_eq!(None, after_iter.next()); 261 | } 262 | 263 | #[test] 264 | fn test_iter_with_enabled() { 265 | let mut s = PlayerBitSet::default(); 266 | let mut iter = s.ones(); 267 | assert_eq!(None, iter.next()); 268 | 269 | s.enable(3); 270 | 271 | let mut after_iter = s.ones(); 272 | assert_eq!(Some(3), after_iter.next()); 273 | assert_eq!(None, after_iter.next()); 274 | } 275 | 276 | #[test] 277 | fn test_display() { 278 | let mut s = PlayerBitSet::new(6); 279 | s.disable(2); 280 | 281 | assert_eq!("[AA_AAA__________]", format!("{s}")) 282 | } 283 | 284 | #[test] 285 | fn test_get() { 286 | let mut s = PlayerBitSet::default(); 287 | 288 | s.enable(0); 289 | s.enable(2); 290 | 291 | assert!(s.get(0)); 292 | assert!(!s.get(1)); 293 | assert!(s.get(2)); 294 | 295 | s.disable(0); 296 | 297 | assert!(!s.get(0)); 298 | assert!(!s.get(1)); 299 | assert!(s.get(2)); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/core/flat_hand.rs: -------------------------------------------------------------------------------- 1 | use crate::core::card::{Card, Suit, Value}; 2 | use std::ops::Index; 3 | use std::ops::{RangeFrom, RangeFull, RangeTo}; 4 | use std::slice::Iter; 5 | 6 | use super::RSPokerError; 7 | 8 | /// Struct to hold cards. 9 | /// 10 | /// This doesn't have the ability to easily check if a card is 11 | /// in the hand. So do that before adding/removing a card. 12 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 13 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 14 | pub struct FlatHand(Vec); 15 | 16 | impl FlatHand { 17 | /// Create the hand with specific hand. 18 | pub fn new_with_cards(cards: Vec) -> Self { 19 | Self(cards) 20 | } 21 | /// From a str create a new hand. 22 | /// 23 | /// # Examples 24 | /// 25 | /// ``` 26 | /// use rs_poker::core::FlatHand; 27 | /// let hand = FlatHand::new_from_str("AdKd").unwrap(); 28 | /// ``` 29 | /// 30 | /// Anything that can't be parsed will return an error. 31 | /// 32 | /// ``` 33 | /// use rs_poker::core::FlatHand; 34 | /// let hand = FlatHand::new_from_str("AdKx"); 35 | /// assert!(hand.is_err()); 36 | /// ``` 37 | pub fn new_from_str(hand_string: &str) -> Result { 38 | // Get the chars iterator. 39 | let mut chars = hand_string.chars(); 40 | // Where we will put the cards 41 | // 42 | // We make the assumption that the hands will have 2 plus five cards. 43 | let mut cards: Vec = Vec::with_capacity(7); 44 | 45 | // Keep looping until we explicitly break 46 | loop { 47 | // Now try and get a char. 48 | let vco = chars.next(); 49 | // If there was no char then we are done. 50 | if vco.is_none() { 51 | break; 52 | } else { 53 | // If we got a value char then we should get a 54 | // suit. 55 | let sco = chars.next(); 56 | // Now try and parse the two chars that we have. 57 | let v = vco 58 | .and_then(Value::from_char) 59 | .ok_or(RSPokerError::UnexpectedValueChar)?; 60 | let s = sco 61 | .and_then(Suit::from_char) 62 | .ok_or(RSPokerError::UnexpectedSuitChar)?; 63 | 64 | let c = Card { value: v, suit: s }; 65 | 66 | match cards.binary_search(&c) { 67 | Ok(_) => return Err(RSPokerError::DuplicateCardInHand(c)), 68 | Err(i) => cards.insert(i, c), 69 | }; 70 | } 71 | } 72 | 73 | if chars.next().is_some() { 74 | return Err(RSPokerError::UnparsedCharsRemaining); 75 | } 76 | 77 | cards.reserve(7); 78 | Ok(Self(cards)) 79 | } 80 | /// Add card at to the hand. 81 | /// No verification is done at all. 82 | pub fn push(&mut self, c: Card) { 83 | self.0.push(c); 84 | } 85 | /// Truncate the hand to the given number of cards. 86 | pub fn truncate(&mut self, len: usize) { 87 | self.0.truncate(len); 88 | } 89 | /// How many cards are in this hand so far ? 90 | pub fn len(&self) -> usize { 91 | self.0.len() 92 | } 93 | /// Are there any cards at all ? 94 | pub fn is_empty(&self) -> bool { 95 | self.0.is_empty() 96 | } 97 | /// Create an iter on the cards. 98 | pub fn iter(&self) -> Iter<'_, Card> { 99 | self.0.iter() 100 | } 101 | } 102 | 103 | impl Default for FlatHand { 104 | /// Create the default empty hand. 105 | fn default() -> Self { 106 | Self(Vec::with_capacity(7)) 107 | } 108 | } 109 | 110 | /// Allow indexing into the hand. 111 | impl Index for FlatHand { 112 | type Output = Card; 113 | fn index(&self, index: usize) -> &Card { 114 | &self.0[index] 115 | } 116 | } 117 | 118 | /// Allow the index to get refernce to every card. 119 | impl Index for FlatHand { 120 | type Output = [Card]; 121 | fn index(&self, range: RangeFull) -> &[Card] { 122 | &self.0[range] 123 | } 124 | } 125 | 126 | impl Index> for FlatHand { 127 | type Output = [Card]; 128 | fn index(&self, index: RangeTo) -> &[Card] { 129 | &self.0[index] 130 | } 131 | } 132 | impl Index> for FlatHand { 133 | type Output = [Card]; 134 | fn index(&self, index: RangeFrom) -> &[Card] { 135 | &self.0[index] 136 | } 137 | } 138 | 139 | impl Extend for FlatHand { 140 | fn extend>(&mut self, iter: T) { 141 | self.0.extend(iter); 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use super::*; 148 | 149 | #[test] 150 | fn test_add_card() { 151 | let mut h = FlatHand::default(); 152 | let c = Card { 153 | value: Value::Three, 154 | suit: Suit::Spade, 155 | }; 156 | h.push(c); 157 | // Make sure that the card was added to the vec. 158 | // 159 | // This will also test that has len works 160 | assert_eq!(1, h.len()); 161 | } 162 | 163 | #[test] 164 | fn test_index() { 165 | let mut h = FlatHand::default(); 166 | h.push(Card { 167 | value: Value::Four, 168 | suit: Suit::Spade, 169 | }); 170 | // Make sure the card is there 171 | assert_eq!( 172 | Card { 173 | value: Value::Four, 174 | suit: Suit::Spade, 175 | }, 176 | h[0] 177 | ); 178 | } 179 | #[test] 180 | fn test_parse_error() { 181 | assert!(FlatHand::new_from_str("BAD").is_err()); 182 | assert!(FlatHand::new_from_str("Adx").is_err()); 183 | } 184 | 185 | #[test] 186 | fn test_parse_one_hand() { 187 | let h = FlatHand::new_from_str("Ad").unwrap(); 188 | assert_eq!(1, h.len()) 189 | } 190 | 191 | #[test] 192 | fn test_parse_empty() { 193 | let h = FlatHand::new_from_str("").unwrap(); 194 | assert!(h.is_empty()); 195 | } 196 | 197 | #[test] 198 | fn test_new_with_cards() { 199 | let h = FlatHand::new_with_cards(vec![ 200 | Card::new(Value::Jack, Suit::Spade), 201 | Card::new(Value::Jack, Suit::Heart), 202 | ]); 203 | 204 | assert_eq!(2, h.len()); 205 | } 206 | 207 | #[test] 208 | fn test_error_on_duplicate_card() { 209 | assert!(FlatHand::new_from_str("AdAd").is_err()); 210 | } 211 | 212 | #[test] 213 | fn test_deterministic_new_from_str() { 214 | let h = FlatHand::new_from_str("AdKd").unwrap(); 215 | 216 | assert_eq!(h, FlatHand::new_from_str("AdKd").unwrap()); 217 | assert_eq!(h, FlatHand::new_from_str("AdKd").unwrap()); 218 | assert_eq!(h, FlatHand::new_from_str("AdKd").unwrap()); 219 | assert_eq!(h, FlatHand::new_from_str("AdKd").unwrap()); 220 | assert_eq!(h, FlatHand::new_from_str("AdKd").unwrap()); 221 | assert_eq!(h, FlatHand::new_from_str("AdKd").unwrap()); 222 | } 223 | 224 | #[test] 225 | fn test_flat_hand_truncate() { 226 | let mut hand = FlatHand::new_with_cards(vec![ 227 | Card::new(Value::Jack, Suit::Spade), 228 | Card::new(Value::Jack, Suit::Heart), 229 | Card::new(Value::Queen, Suit::Diamond), 230 | ]); 231 | assert_eq!(hand.len(), 3); 232 | 233 | hand.truncate(2); 234 | 235 | assert_eq!(hand.len(), 2); 236 | 237 | assert_eq!(hand[0], Card::new(Value::Jack, Suit::Spade)); 238 | assert_eq!(hand[1], Card::new(Value::Jack, Suit::Heart)); 239 | 240 | hand.truncate(0); 241 | 242 | assert_eq!(hand.len(), 0); 243 | assert!(hand.is_empty()); 244 | assert_eq!(hand[0..], []); 245 | } 246 | 247 | #[test] 248 | fn test_extend() { 249 | let mut hand = FlatHand::new_with_cards(vec![ 250 | Card::new(Value::Jack, Suit::Spade), 251 | Card::new(Value::Jack, Suit::Heart), 252 | ]); 253 | 254 | assert_eq!(hand.len(), 2); 255 | 256 | hand.extend(vec![ 257 | Card::new(Value::Queen, Suit::Diamond), 258 | Card::new(Value::King, Suit::Club), 259 | ]); 260 | 261 | assert_eq!(hand.len(), 4); 262 | 263 | assert_eq!(hand[0], Card::new(Value::Jack, Suit::Spade)); 264 | assert_eq!(hand[1], Card::new(Value::Jack, Suit::Heart)); 265 | assert_eq!(hand[2], Card::new(Value::Queen, Suit::Diamond)); 266 | assert_eq!(hand[3], Card::new(Value::King, Suit::Club)); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! RS-Poker is a library for poker 2 | //! Currently RS-Poker supports: 3 | //! 4 | //! * Hand Iteration. 5 | //! * Hand Ranking. 6 | //! * Hand Range parsing. 7 | //! * Hand Range generation. 8 | //! * ICM tournament values 9 | //! * Monte carlo holdem 10 | //! * Holdem Game State with action validation 11 | //! * Holdem agents 12 | //! * Holdem game simulation 13 | //! 14 | //! Our focus is on correctness and performance. 15 | //! 16 | //! ## Core library 17 | //! 18 | //! The core of the library contains code that is relevant 19 | //! to all poker variants. Card suits, values, hand 20 | //! values, and datastructures used in other parts of the crate. 21 | //! 22 | //! ## Holdem 23 | //! 24 | //! Holdem is the best supported variant. 25 | //! 26 | //! ### Starting Hands 27 | //! 28 | //! The StartingHand module contains the following key components: 29 | //! 30 | //! `Suitedness`: This is an enum type that represents how the suits of a hand 31 | //! correspond to each other. It has three variants: 32 | //! 33 | //! * Suited: All of the cards are the same suit. 34 | //! * OffSuit: None of the cards are the same suit. 35 | //! * Any: Makes no promises about the suit. 36 | //! 37 | //! `HoldemStartingHand`: This represents the two-card starting hand of 38 | //! Texas Hold'em. It can generate all possible actual starting 39 | //! hands given two values and a suitedness condition. 40 | //! 41 | //! ```rust 42 | //! use rs_poker::core::Value; 43 | //! use rs_poker::holdem::{StartingHand, Suitedness}; 44 | //! 45 | //! let hands = StartingHand::default(Value::Ace, Value::Ace, Suitedness::OffSuit).possible_hands(); 46 | //! assert_eq!(6, hands.len()); 47 | //! ``` 48 | //! 49 | //! ### Range parsing 50 | //! 51 | //! A lot of discussion online is around ranges. For example: 52 | //! "High Jack had a range of KQo+ and 99+" 53 | //! 54 | //! The range parsing module allows turning those range strings into vectors of 55 | //! possible hands. 56 | //! 57 | //! ``` 58 | //! use rs_poker::holdem::RangeParser; 59 | //! let hands = RangeParser::parse_one("KQo+").unwrap(); 60 | //! 61 | //! // There are 24 different combinations of off suit 62 | //! // connectors King + Queen or higher 63 | //! assert_eq!(24, hands.len()); 64 | //! ``` 65 | //! ### Monte Carlo Game simulation 66 | //! 67 | //! Sometimes it's important to know your expected equity 68 | //! in a pot vs a given set of card. In doing that it's useful 69 | //! to quickly simulate what could happen. 70 | //! 71 | //! The `MonteCarloGame` strcut does that: 72 | //! 73 | //! ``` rust 74 | //! use rs_poker::core::{Card, Hand, Suit, Value}; 75 | //! use rs_poker::holdem::MonteCarloGame; 76 | //! 77 | //! let hero = Hand::new_with_cards(vec![ 78 | //! Card::new(Value::Jack, Suit::Spade), 79 | //! Card::new(Value::Jack, Suit::Heart), 80 | //! ]); 81 | //! let villan = Hand::new_with_cards(vec![ 82 | //! Card::new(Value::Ace, Suit::Spade), 83 | //! Card::new(Value::King, Suit::Spade), 84 | //! ]); 85 | //! let mut monte_sim = MonteCarloGame::new(vec![hero, villan]).unwrap(); 86 | //! let mut wins: [u64; 2] = [0, 0]; 87 | //! for _ in 0..100_000 { 88 | //! let r = monte_sim.simulate(); 89 | //! monte_sim.reset(); 90 | //! // You can handle ties however you like here 91 | //! wins[r.0.ones().next().unwrap()] += 1 92 | //! } 93 | //! 94 | //! // Jacks hold up most of the time 95 | //! assert!(wins[0] > wins[1]); 96 | //! ``` 97 | //! 98 | //! ## Simulated ICM 99 | //! 100 | //! Not all chips are equal; when rewards for tounaments are highly in favor of 101 | //! placing higher, sometimes the correct decision comes down to expected value 102 | //! of the whole tournament. 103 | //! 104 | //! ``` 105 | //! use rand::{Rng, rng}; 106 | //! use rs_poker::simulated_icm::simulate_icm_tournament; 107 | //! 108 | //! let payments = vec![10_000, 6_000, 4_000, 1_000, 800]; 109 | //! let mut rng = rng(); 110 | //! let chips: Vec = (0..4).map(|_| rng.random_range(100..300_000)).collect(); 111 | //! let simulated_results = simulate_icm_tournament(&chips, &payments); 112 | //! 113 | //! // There's one payout per player still remaining. 114 | //! // You can run this over and over again to get an 115 | //! // average expected value. 116 | //! assert_eq!(chips.len(), simulated_results.len()); 117 | //! ``` 118 | //! 119 | //! ## Holdem arena 120 | //! 121 | //! Can you program a bot that can beat the best poker players in 122 | //! the world? This is your starting place to do that. Implement on Trait 123 | //! `Agent` and you can simulate Texas Holdem games with your agent. 124 | //! 125 | //! ```rust,ignore 126 | //! fn act(&mut self, id: u128, game_state: &GameState) -> AgentAction; 127 | //! ``` 128 | //! 129 | //! Your agent takes in the current game ID and the current game state. From 130 | //! there it returns what action it would like to play. If you're agent is 131 | //! better than others it will have +EV and win more money. 132 | //! 133 | //! The arena is code to simulate different strategies and get outcomes. It 134 | //! includes utilities to run tournaments of agents to see who would win in an 135 | //! elimination style game. 136 | //! 137 | //! For example if you want to simulate the different between different vpip's. 138 | //! Simply code an agent with configurable starting hand range and see what the 139 | //! expected values are. The arena is configurable for number of players from 140 | //! heads up all the way to full ring. 141 | //! 142 | //! ### Stability 143 | //! 144 | //! The holdem arena is the newest addition to `rs-poker` and the most 145 | //! experimental. So it's the most likely to change in the future. 146 | //! 147 | //! ### Internals 148 | //! 149 | //! The arena has several parts: 150 | //! * `GameState` this holds the current state of all the chips, bets, player 151 | //! all in status, and if players are active in a hand or round. 152 | //! * `Agent` is the trait needed to implement different automatic players in 153 | //! the poker. 154 | //! * `Historian` is the trait implemented to recieve actions just after they 155 | //! are applied to the gamestate. This allows the historian to follow along 156 | //! and record the game state. 157 | //! * `HoldemSimulation` this is the main wrapper struct that handles calling 158 | //! the agents and force folding the agents for any invalid actions. 159 | //! * `HoldemSimulationBuilder` that is used to construcst single simulations. 160 | //! `GameState` and `Agents` are required the rest are optional 161 | //! * `HoldemCompetition` that keeps track of all simulation based stats from 162 | //! simluations genreated via `HoldemSimulationGenerator`. 163 | //! * Each `HoldemSimulationGenerator` is built of `AgentsGenerator`, 164 | //! `HistorianGenerator`, and `GameStateGenerator` 165 | //! 166 | //! 167 | //! ### Example 168 | //! 169 | //! ``` 170 | //! use rs_poker::arena::{ 171 | //! AgentGenerator, CloneGameStateGenerator, GameState, 172 | //! agent::{CallingAgentGenerator, RandomAgentGenerator}, 173 | //! competition::{HoldemCompetition, StandardSimulationIterator}, 174 | //! }; 175 | //! let agent_gens: Vec> = vec![ 176 | //! Box::::default(), 177 | //! Box::::default(), 178 | //! Box::::default(), 179 | //! ]; 180 | //! let stacks = vec![100.0; 3]; 181 | //! let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0); 182 | //! let sim_gen = StandardSimulationIterator::new( 183 | //! agent_gens, 184 | //! vec![], // no historians 185 | //! CloneGameStateGenerator::new(game_state), 186 | //! ); 187 | //! let mut competition = HoldemCompetition::new(sim_gen); 188 | //! let _first_results = competition.run(100).unwrap(); 189 | //! ``` 190 | #![cfg_attr(feature = "arena", feature(assert_matches))] 191 | #![deny(clippy::all)] 192 | 193 | extern crate rand; 194 | 195 | /// Allow all the core poker functionality to be used 196 | /// externally. Everything in core should be agnostic 197 | /// to poker style. 198 | pub mod core; 199 | /// The holdem specific code. This contains range 200 | /// parsing, game state, and starting hand code. 201 | pub mod holdem; 202 | 203 | /// Given a tournament calculate the implied 204 | /// equity in the total tournament. 205 | pub mod simulated_icm; 206 | 207 | #[cfg(feature = "arena")] 208 | pub mod arena; 209 | 210 | /// Open Hand History (OHH) implementation 211 | /// 212 | /// Supports reading and writing poker hand histories in the Open Hand History 213 | /// format. This format is a standardized JSON format for storing poker hand 214 | /// histories that is poker site agnostic and supports: 215 | /// 216 | /// * Cash games and tournaments 217 | /// * Multiple poker variants 218 | /// * Different betting structures 219 | /// * Tournament specific features like bounties 220 | /// * Detailed player actions and timing 221 | /// 222 | /// See https://hh-specs.handhistory.org/ for the full specification. 223 | #[cfg(feature = "open-hand-history")] 224 | pub mod open_hand_history; 225 | -------------------------------------------------------------------------------- /src/holdem/starting_hand.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{Card, FlatHand, Suit, Value}; 2 | 3 | /// Enum to represent how the suits of a hand correspond to each other. 4 | /// `Suitedness::Suited` will mean that all cards have the same suit 5 | /// `Suitedness::OffSuit` will mean that all cards have the different suit 6 | /// `Suitedness::Any` makes no promises. 7 | #[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Clone, Copy)] 8 | pub enum Suitedness { 9 | /// All of the cards are the same suit 10 | Suited, 11 | /// None of the cards are the same suit 12 | OffSuit, 13 | /// No promises about suit. 14 | Any, 15 | } 16 | 17 | /// `HoldemStartingHand` represents the two card starting hand of texas holdem. 18 | /// It can generate all the possible actual starting hands. 19 | /// 20 | /// Give two values and if you only want suited variants. 21 | #[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Clone)] 22 | pub struct Default { 23 | /// The first value. 24 | value_one: Value, 25 | /// The second value. 26 | value_two: Value, 27 | /// should we only consider possible starting hands of the same suit? 28 | suited: Suitedness, 29 | } 30 | 31 | impl Default { 32 | /// Is this starting hand a pocket pair? 33 | fn is_pair(&self) -> bool { 34 | self.value_one == self.value_two 35 | } 36 | 37 | /// Create a new vector of all suited hands. 38 | fn create_suited(&self) -> Vec { 39 | // Can't have a suited pair. Not unless you're cheating. 40 | if self.is_pair() { 41 | return vec![]; 42 | } 43 | Suit::suits() 44 | .iter() 45 | .map(|s| { 46 | FlatHand::new_with_cards(vec![ 47 | Card { 48 | value: self.value_one, 49 | suit: *s, 50 | }, 51 | Card { 52 | value: self.value_two, 53 | suit: *s, 54 | }, 55 | ]) 56 | }) 57 | .collect() 58 | } 59 | 60 | /// Create a new vector of all the off suit hands. 61 | fn create_offsuit(&self) -> Vec { 62 | // Since the values are the same there is no reason to swap the suits. 63 | let expected_hands = if self.is_pair() { 6 } else { 12 }; 64 | self.append_offsuit(Vec::with_capacity(expected_hands)) 65 | } 66 | 67 | /// Append all the off suit hands to the passed in vec and 68 | /// then return it. 69 | /// 70 | /// @returns the passed in vector with offsuit hands appended. 71 | fn append_offsuit(&self, mut hands: Vec) -> Vec { 72 | let suits = Suit::suits(); 73 | for (i, suit_one) in suits.iter().enumerate() { 74 | for suit_two in &suits[i + 1..] { 75 | // Push the hands in. 76 | hands.push(FlatHand::new_with_cards(vec![ 77 | Card { 78 | value: self.value_one, 79 | suit: *suit_one, 80 | }, 81 | Card { 82 | value: self.value_two, 83 | suit: *suit_two, 84 | }, 85 | ])); 86 | 87 | // If this isn't a pair then the flipped suits is needed. 88 | if self.value_one != self.value_two { 89 | hands.push(FlatHand::new_with_cards(vec![ 90 | Card { 91 | value: self.value_one, 92 | suit: *suit_two, 93 | }, 94 | Card { 95 | value: self.value_two, 96 | suit: *suit_one, 97 | }, 98 | ])); 99 | } 100 | } 101 | } 102 | hands 103 | } 104 | 105 | /// Get all the possible starting hands represented by the 106 | /// two values of this starting hand. 107 | fn possible_hands(&self) -> Vec { 108 | match self.suited { 109 | Suitedness::Suited => self.create_suited(), 110 | Suitedness::OffSuit => self.create_offsuit(), 111 | Suitedness::Any => self.append_offsuit(self.create_suited()), 112 | } 113 | } 114 | } 115 | 116 | /// Starting hand struct to represent where it's one 117 | /// static card and a range for the other. 118 | #[derive(Debug, PartialEq, Eq, PartialOrd)] 119 | pub struct SingleCardRange { 120 | /// First value; this one will not change. 121 | value_one: Value, 122 | /// Inclusive start range 123 | start: Value, 124 | /// Inclusive end range 125 | end: Value, 126 | /// What Suits can this have. 127 | suited: Suitedness, 128 | } 129 | 130 | impl SingleCardRange { 131 | /// Generate all the possible hands for this starting hand type. 132 | fn possible_hands(&self) -> Vec { 133 | let mut cur_value = self.start; 134 | let mut hands = vec![]; 135 | // TODO: Make a better iterator for values. 136 | while cur_value <= self.end { 137 | let mut new_hands = Default { 138 | value_one: self.value_one, 139 | value_two: cur_value, 140 | suited: self.suited, 141 | } 142 | .possible_hands(); 143 | hands.append(&mut new_hands); 144 | cur_value = Value::from_u8(cur_value as u8 + 1); 145 | } 146 | 147 | hands 148 | } 149 | } 150 | 151 | /// Enum to represent all the possible ways to specify a starting hand. 152 | #[derive(Debug, PartialEq, Eq, PartialOrd)] 153 | pub enum StartingHand { 154 | /// Default starting hand type. This means that we 155 | /// specify two cards and their suitedness. 156 | Def(Default), 157 | 158 | /// A starting hand where the second card is a range. 159 | SingleCardRange(SingleCardRange), 160 | } 161 | 162 | impl StartingHand { 163 | /// Create a default starting hand with two `Value`'s and a `Suitedness`. 164 | pub fn default(value_one: Value, value_two: Value, suited: Suitedness) -> Self { 165 | Self::Def(Default { 166 | value_one, 167 | value_two, 168 | suited, 169 | }) 170 | } 171 | 172 | /// Create a new StartingHand with the second card being a range. 173 | pub fn single_range(value_one: Value, start: Value, end: Value, suited: Suitedness) -> Self { 174 | Self::SingleCardRange(SingleCardRange { 175 | value_one, 176 | start, 177 | end, 178 | suited, 179 | }) 180 | } 181 | 182 | /// Create every possible unique StartingHand. 183 | pub fn all() -> Vec { 184 | let mut hands = Vec::with_capacity(169); 185 | let values = Value::values(); 186 | for (i, value_one) in values.iter().enumerate() { 187 | for value_two in &values[i..] { 188 | hands.push(Self::Def(Default { 189 | value_one: *value_one, 190 | value_two: *value_two, 191 | suited: Suitedness::OffSuit, 192 | })); 193 | if value_one != value_two { 194 | hands.push(Self::Def(Default { 195 | value_one: *value_one, 196 | value_two: *value_two, 197 | suited: Suitedness::Suited, 198 | })); 199 | } 200 | } 201 | } 202 | hands 203 | } 204 | 205 | /// From a `StartingHand` specify all the hands this could represent. 206 | pub fn possible_hands(&self) -> Vec { 207 | match *self { 208 | Self::Def(ref h) => h.possible_hands(), 209 | Self::SingleCardRange(ref h) => h.possible_hands(), 210 | } 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | use super::*; 217 | 218 | #[test] 219 | fn test_aces() { 220 | let sh = Default { 221 | value_one: Value::Ace, 222 | value_two: Value::Ace, 223 | suited: Suitedness::OffSuit, 224 | }; 225 | assert!(6 == sh.possible_hands().len()); 226 | } 227 | 228 | #[test] 229 | fn test_suited_connector() { 230 | let sh = Default { 231 | value_one: Value::Ace, 232 | value_two: Value::King, 233 | suited: Suitedness::Suited, 234 | }; 235 | assert!(4 == sh.possible_hands().len()); 236 | } 237 | #[test] 238 | fn test_unsuited_connector() { 239 | let sh = Default { 240 | value_one: Value::Ace, 241 | value_two: Value::King, 242 | suited: Suitedness::OffSuit, 243 | }; 244 | assert!(12 == sh.possible_hands().len()); 245 | } 246 | 247 | #[test] 248 | fn test_starting_hand_count() { 249 | let num_to_test: usize = StartingHand::all() 250 | .iter() 251 | .map(|h| h.possible_hands().len()) 252 | .sum(); 253 | assert!(1326 == num_to_test); 254 | } 255 | } 256 | --------------------------------------------------------------------------------