Skip to content

Commit

Permalink
perf: usee bitsets for deck and hands
Browse files Browse the repository at this point in the history
Summary:
- Move Deck to use CardBitSet
- Add FlatHand that allows indexing and ordering of cards  (it follow
  FlatDeck's example)
- Add Hand that uses CardBitSet to do it's thing
- Move to using Hand and Deck in Simulation

Test Plan:

- CI
- Tests added
  • Loading branch information
elliottneilclark committed Jan 3, 2025
1 parent 3bdf181 commit 49e99ac
Show file tree
Hide file tree
Showing 17 changed files with 508 additions and 286 deletions.
6 changes: 3 additions & 3 deletions benches/rank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ extern crate rand;
extern crate rs_poker;

use criterion::Criterion;
use rs_poker::core::{Deck, FlatDeck, Hand, Rankable};
use rs_poker::core::{Deck, FlatDeck, FlatHand, Rankable};

fn rank_one(c: &mut Criterion) {
let d: FlatDeck = Deck::default().into();
let hand = Hand::new_with_cards(d.sample(5));
let hand = FlatHand::new_with_cards(d.sample(5));
c.bench_function("Rank one 5 card hand", move |b| b.iter(|| hand.rank()));
}

fn rank_best_seven(c: &mut Criterion) {
let d: FlatDeck = Deck::default().into();
let hand = Hand::new_with_cards(d.sample(7));
let hand = FlatHand::new_with_cards(d.sample(7));
c.bench_function("Rank best 5card hand from 7", move |b| {
b.iter(|| hand.rank())
});
Expand Down
4 changes: 2 additions & 2 deletions fuzz/fuzz_targets/fuzzer_script_2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
#[macro_use]
extern crate libfuzzer_sys;
extern crate rs_poker;
use rs_poker::core::Hand;
use rs_poker::core::FlatHand;
use std::str;

fuzz_target!(|data: &[u8]| {
if let Ok(s) = str::from_utf8(data) {
if let Ok(h) = Hand::new_from_str(s) {
if let Ok(h) = FlatHand::new_from_str(s) {
assert!(s.len() >= h.len());
}
}
Expand Down
2 changes: 1 addition & 1 deletion fuzz/fuzz_targets/rank_seven.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::str;

fuzz_target!(|data: &[u8]| {
if let Ok(s) = str::from_utf8(data) {
if let Ok(h) = Hand::new_from_str(s) {
if let Ok(h) = FlatHand::new_from_str(s) {
if h.len() == 7 {
let r_seven = h.rank();
let r_five_max = CardIter::new(&h[..], 5)
Expand Down
8 changes: 5 additions & 3 deletions src/arena/agent/random.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ impl RandomPotControlAgent {
}

fn clean_hands(&self, game_state: &GameState) -> Vec<Hand> {
let default_hand = Hand::new_with_cards(game_state.board.clone());
let mut default_hand = Hand::new();
// Copy the board into the default hand
default_hand.extend(game_state.board.iter().cloned());

let to_act_idx = game_state.to_act_idx();
game_state
Expand Down Expand Up @@ -259,8 +261,8 @@ mod tests {

// Add two random cards to every hand.
for hand in game_state.hands.iter_mut() {
hand.push(deck.deal().unwrap());
hand.push(deck.deal().unwrap());
hand.insert(deck.deal().unwrap());
hand.insert(deck.deal().unwrap());
}

let mut sim = HoldemSimulationBuilder::default()
Expand Down
4 changes: 2 additions & 2 deletions src/arena/game_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ impl GameState {
// Count all the money in the pot.
total_pot += *bet;

// Handle the case that they have no money left
// FlatHandle the case that they have no money left
if *stack <= 0.0 {
if *bet > 0.0 && round != Round::Starting {
// If the player is out of money and they've put money in
Expand Down Expand Up @@ -378,7 +378,7 @@ impl GameState {
round_data,
// No board cards
vec![],
// Hands are empty
// FlatHands are empty
vec![Hand::default(); num_players],
// Current stacks
stacks,
Expand Down
6 changes: 3 additions & 3 deletions src/arena/sim_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn build_flat_deck<R: Rng>(game_state: &GameState, rng: &mut R) -> FlatDeck {

for hand in game_state.hands.iter() {
for card in hand.iter() {
d.remove(*card);
d.remove(card);
}
}
let mut flat_deck: FlatDeck = d.into();
Expand Down Expand Up @@ -356,15 +356,15 @@ mod tests {
let c = Card::try_from(card_str).unwrap();
assert!(deck.contains(c));
deck.remove(c);
game_state.hands[idx].push(c);
game_state.hands[idx].insert(c);
}

fn deal_community_card(card_str: &str, deck: &mut CardBitSet, game_state: &mut GameState) {
let c = Card::try_from(card_str).unwrap();
assert!(deck.contains(c));
deck.remove(c);
for h in &mut game_state.hands {
h.push(c);
h.insert(c);
}

game_state.board.push(c);
Expand Down
7 changes: 5 additions & 2 deletions src/arena/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ impl HoldemSimulation {
let action = self.agents[idx].act(&self.id, &self.game_state);

event!(parent: &span, Level::TRACE, ?action, idx);
self.run_agent_action(action)
self.run_agent_action(action);
}

/// Given the action that an agent wants to take, this function will
Expand Down Expand Up @@ -638,7 +638,10 @@ impl HoldemSimulation {
// Some user might never error.
// For them it's a panic.
if self.panic_on_historian_error {
panic!("Historian error {}", error);
panic!(
"Historian error {}\naction={:?}\ngame_state = {:?}",
error, action, self.game_state
);
}
None
}
Expand Down
54 changes: 54 additions & 0 deletions src/core/card_bit_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::ops::{BitOr, BitOrAssign, BitXor, BitXorAssign};
use super::{Card, FlatDeck};
use std::fmt::Debug;

#[cfg(feature = "serde")]
use serde::ser::SerializeSeq;

/// This struct is a bitset for cards
/// Each card is represented by a bit in a 64 bit integer
///
Expand Down Expand Up @@ -112,6 +115,10 @@ impl CardBitSet {
pub fn count(&self) -> usize {
self.cards.count_ones() as usize
}

pub fn clear(&mut self) {
self.cards = 0;
}
}

impl Default for CardBitSet {
Expand Down Expand Up @@ -248,6 +255,53 @@ impl Iterator for CardBitSetIter {
}
}

#[cfg(feature = "serde")]
impl serde::Serialize for CardBitSet {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.count()))?;
for card in (*self).into_iter() {
seq.serialize_element(&card)?;
}
seq.end()
}
}

#[cfg(feature = "serde")]
struct CardBitSetVisitor;

#[cfg(feature = "serde")]
impl<'de> serde::de::Visitor<'de> for CardBitSetVisitor {
type Value = CardBitSet;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a sequence of cards")
}

fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut deck = CardBitSet::new();
while let Some(card) = seq.next_element()? {
deck.insert(card);
}
Ok(deck)
}
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for CardBitSet {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_seq(CardBitSetVisitor)
}
}

#[cfg(test)]
mod tests {
use std::collections::HashSet;
Expand Down
4 changes: 2 additions & 2 deletions src/core/card_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ mod tests {

#[test]
fn test_iter_one() {
let mut h = Hand::default();
let mut h = FlatHand::default();
h.push(Card {
value: Value::Two,
suit: Suit::Spade,
Expand All @@ -113,7 +113,7 @@ mod tests {

#[test]
fn test_iter_two() {
let mut h = Hand::default();
let mut h = FlatHand::default();
h.push(Card {
value: Value::Two,
suit: Suit::Spade,
Expand Down
63 changes: 29 additions & 34 deletions src/core/deck.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::core::card::{Card, Suit, Value};
use std::collections::hash_set::{IntoIter, Iter};
use std::collections::HashSet;
use crate::core::card::Card;

use super::{CardBitSet, CardBitSetIter};

/// Deck struct that can tell quickly if a card is in the deck
///
Expand Down Expand Up @@ -36,14 +36,10 @@ use std::collections::HashSet;
/// println!("{:?}", card);
/// }
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct Deck {
/// Card storage.
/// Used to figure out quickly
/// if this card is in the deck.
cards: HashSet<Card>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Deck(CardBitSet);

impl Deck {
/// Create a new empty deck
Expand All @@ -59,43 +55,49 @@ impl Deck {
/// assert_eq!(0, deck.len());
/// ```
pub fn new() -> Self {
Self {
cards: HashSet::new(),
}
Self(CardBitSet::new())
}
/// Given a card, is it in the current deck?
pub fn contains(&self, c: &Card) -> bool {
self.cards.contains(c)
self.0.contains(*c)
}
/// Given a card remove it from the deck if it is present.
pub fn remove(&mut self, c: &Card) -> bool {
self.cards.remove(c)
let contains = self.contains(c);
self.0.remove(*c);
contains
}
/// Add a given card to the deck.
pub fn insert(&mut self, c: Card) -> bool {
self.cards.insert(c)
let contains = self.contains(&c);
self.0.insert(c);
!contains
}
/// How many cards are there in the deck.
pub fn len(&self) -> usize {
self.cards.len()
pub fn count(&self) -> usize {
self.0.count()
}
/// Have all of the cards been dealt from this deck?
pub fn is_empty(&self) -> bool {
self.cards.is_empty()
self.0.is_empty()
}
/// Get an iterator from this deck
pub fn iter(&self) -> Iter<Card> {
self.cards.iter()
pub fn iter(&self) -> CardBitSetIter {
self.0.into_iter()
}

pub fn len(&self) -> usize {
self.0.count()
}
}

/// Turn a deck into an iterator
impl IntoIterator for Deck {
type Item = Card;
type IntoIter = IntoIter<Card>;
type IntoIter = CardBitSetIter;
/// Consume this deck and create a new iterator.
fn into_iter(self) -> IntoIter<Card> {
self.cards.into_iter()
fn into_iter(self) -> CardBitSetIter {
self.0.into_iter()
}
}

Expand All @@ -108,21 +110,14 @@ impl Default for Deck {
/// assert_eq!(52, Deck::default().len());
/// ```
fn default() -> Self {
let mut cards: HashSet<Card> = HashSet::new();
for v in &Value::values() {
for s in &Suit::suits() {
cards.insert(Card {
value: *v,
suit: *s,
});
}
}
Self { cards }
Self(CardBitSet::default())
}
}

#[cfg(test)]
mod tests {
use crate::core::{Suit, Value};

use super::*;

#[test]
Expand Down
Loading

0 comments on commit 49e99ac

Please sign in to comment.