From: Geoffrey Allott Date: Sun, 4 Jun 2023 23:58:37 +0000 (+0100) Subject: implement a compact CardSet to replace HashSet X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=ddc7ee26e2fb428cbb152672e52d32f8e38c26cc;p=pokerwave.git implement a compact CardSet to replace HashSet --- diff --git a/src/card.rs b/src/card.rs index 9a26e8c..673c864 100644 --- a/src/card.rs +++ b/src/card.rs @@ -85,12 +85,136 @@ pub struct Card { pub suit: Suit, } +impl Card { + fn from_trailing_zeros(i: u32) -> Option { + Some(Card { + rank: match i / 4 + 2 { + 2 => Rank::Two, + 3 => Rank::Three, + 4 => Rank::Four, + 5 => Rank::Five, + 6 => Rank::Six, + 7 => Rank::Seven, + 8 => Rank::Eight, + 9 => Rank::Nine, + 10 => Rank::Ten, + 11 => Rank::Jack, + 12 => Rank::Queen, + 13 => Rank::King, + 14 => Rank::Ace, + _ => return None, + }, + suit: match i % 4 { + 0 => Suit::Clubs, + 1 => Suit::Diamonds, + 2 => Suit::Hearts, + 3 => Suit::Spades, + _ => return None, + }, + }) + } + + fn repr(&self) -> u64 { + 1 << (self.suit as u64 + 4 * (self.rank as u64 - 2)) + } +} + impl Display for Card { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}{}", self.rank, self.suit) } } +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct CardSet { + repr: u64, +} + +impl CardSet { + pub fn new() -> CardSet { + CardSet { repr: 0 } + } + + pub fn complete_deck() -> CardSet { + CardSet { repr: 0b1111111111111_1111111111111_1111111111111_1111111111111 } + } + + pub fn is_empty(&self) -> bool { + self.repr == 0 + } + + pub fn len(&self) -> usize { + self.repr.count_ones() as usize + } + + pub fn clear(&mut self) { + self.repr = 0; + } + + pub fn insert(&mut self, card: Card) -> bool { + let card_repr = card.repr(); + if self.repr & card_repr != 0 { + false + } else { + self.repr |= card_repr; + true + } + } + + pub fn contains(&self, card: &Card) -> bool { + self.repr & card.repr() != 0 + } + + pub fn remove(&mut self, card: &Card) -> bool { + let card_repr = card.repr(); + if self.repr & card_repr == 0 { + false + } else { + self.repr &= !card_repr; + true + } + } +} + +pub struct CardSetIntoIter { + inner: CardSet, +} + +impl IntoIterator for CardSet { + type IntoIter = CardSetIntoIter; + type Item = Card; + + fn into_iter(self) -> Self::IntoIter { + CardSetIntoIter { inner: self } + } +} + +impl Iterator for CardSetIntoIter { + type Item = Card; + + fn next(&mut self) -> Option { + if self.inner.repr == 0 { + None + } else { + let i = self.inner.repr.trailing_zeros(); + self.inner.repr &= !(1 << i); + Card::from_trailing_zeros(i) + } + } +} + +impl FromIterator for CardSet { + fn from_iter(iter: T) -> Self + where T: IntoIterator + { + let mut set = CardSet::new(); + for card in iter { + set.insert(card); + } + set + } +} + pub const TWO_OF_CLUBS: Card = Card { rank: Two, suit: Clubs }; pub const THREE_OF_CLUBS: Card = Card { rank: Three, suit: Clubs }; pub const FOUR_OF_CLUBS: Card = Card { rank: Four, suit: Clubs }; diff --git a/src/game/cribbage/mod.rs b/src/game/cribbage/mod.rs index 6c0eb54..7d041cc 100644 --- a/src/game/cribbage/mod.rs +++ b/src/game/cribbage/mod.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use crate::card::{Card, FIFTY_TWO_CARD_DECK, Rank}; +use crate::card::{Card, CardSet, Rank}; use crate::rng::{Seed, WaveRng}; use crate::seats::Seats; use crate::username::Username; @@ -51,11 +51,11 @@ pub struct Cribbage { seats: Seats, dealer: Option, receiver: Option, - deck: HashSet, - hands: HashMap>, + deck: CardSet, + hands: HashMap, turn_up: Option, - box_cards: HashSet, - revealed: HashSet, + box_cards: CardSet, + revealed: CardSet, pegging_cards: Vec<(Username, Card)>, used_pegging_cards: Vec<(Username, Card)>, players_still_in: HashSet, @@ -74,11 +74,11 @@ impl Cribbage { seats: Seats::new(), dealer: None, receiver: None, - deck: FIFTY_TWO_CARD_DECK.iter().cloned().collect(), + deck: CardSet::complete_deck(), hands: HashMap::new(), turn_up: None, - box_cards: HashSet::new(), - revealed: HashSet::new(), + box_cards: CardSet::new(), + revealed: CardSet::new(), pegging_cards: Vec::new(), used_pegging_cards: Vec::new(), players_still_in: HashSet::new(), @@ -92,15 +92,15 @@ impl Cribbage { } fn hand_size(&self, username: Username) -> usize { - self.hands.get(&username).map_or(0, HashSet::len) + self.hands.get(&username).map_or(0, CardSet::len) } fn all_hands_are_empty(&self) -> bool { - self.hands.values().all(HashSet::is_empty) + self.hands.values().all(CardSet::is_empty) } fn all_hands_dealt(&self) -> bool { - self.hands.values().map(HashSet::len).all(|len| len == 6) + self.hands.values().map(CardSet::len).all(|len| len == 6) } fn pegging_total(&self) -> u32 { @@ -109,7 +109,7 @@ impl Cribbage { fn hand_has_playable_pegging_card(&self, username: Username) -> bool { let total = self.pegging_total(); - self.hands.get(&username).map_or(false, |hand| hand.iter().any(|card| total + value(card.rank) <= 31)) + self.hands.get(&username).map_or(false, |hand| hand.into_iter().any(|card| total + value(card.rank) <= 31)) } fn next_player_still_in(&self) -> Option { @@ -133,14 +133,14 @@ impl Cribbage { } fn unrevealed_card(&self, username: Username) -> Option { - self.hands.get(&username).and_then(|hand| hand.iter().filter(|card| !self.revealed.contains(*card)).min().copied()) + self.hands.get(&username).and_then(|hand| hand.into_iter().filter(|card| !self.revealed.contains(card)).min()) } fn four_card_hand(&self, username: Username) -> Option<[Card; 4]> { self.hands.get(&username).and_then(|hand| { let hand: Vec<_> = hand.into_iter().collect(); if let [a, b, c, d] = hand[..] { - Some([*a, *b, *c, *d]) + Some([a, b, c, d]) } else { None } @@ -148,13 +148,13 @@ impl Cribbage { } fn unrevealed_box_card(&self) -> Option { - self.box_cards.iter().filter(|card| !self.revealed.contains(*card)).min().copied() + self.box_cards.into_iter().filter(|card| !self.revealed.contains(card)).min() } fn four_card_box(&self) -> Option<[Card; 4]> { - let hand: Vec<_> = self.box_cards.iter().collect(); + let hand: Vec<_> = self.box_cards.into_iter().collect(); if let [a, b, c, d] = hand[..] { - Some([*a, *b, *c, *d]) + Some([a, b, c, d]) } else { None } @@ -165,7 +165,7 @@ impl Cribbage { } fn winner(&self) -> Option { - self.points.iter().filter(|&(_, points)| *points >= self.settings.target_score).next().map(|(username, _)| *username) + self.points.iter().find(|&(_, points)| *points >= self.settings.target_score).map(|(username, _)| *username) } } @@ -258,7 +258,7 @@ impl Game for Cribbage { (State::NotStarted, Action::Leave) => self.seats.remove_player(username), (_, Action::NextToDeal) => { self.dealer = Some(username); - self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect(); + self.deck = CardSet::complete_deck(); self.turn_up = None; self.hands.clear(); self.receiver = self.seats.player_after(username); @@ -338,7 +338,7 @@ impl Game for Cribbage { match self.next_player_still_in() { None => { self.state = State::ScoringPegging; - self.used_pegging_cards.extend(self.pegging_cards.drain(..)); + self.used_pegging_cards.append(&mut self.pegging_cards); self.players_still_in = self.hands.iter().filter(|(_, cards)| !cards.is_empty()).map(|(&username, _)| username).collect(); }, active => self.active = active, @@ -348,7 +348,7 @@ impl Game for Cribbage { (State::ScoringPegging, Action::Score { points, .. }) => { *self.points.entry(username).or_default() += points as u32; if self.next_player_still_in().is_none() || self.pegging_total() == 31 { - self.used_pegging_cards.extend(self.pegging_cards.drain(..)); + self.used_pegging_cards.append(&mut self.pegging_cards); self.players_still_in = self.hands.iter().filter(|(_, cards)| !cards.is_empty()).map(|(&username, _)| username).collect(); } if self.player_has_won(username) { @@ -418,11 +418,11 @@ impl Game for Cribbage { }, State::Dealing => { if let Some(username) = self.receiver { - let card = rng.choose_from(&self.deck).cloned(); + let card = rng.choose_from(self.deck); DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::ReceiveCard { card } })) } else if let Some(username) = self.dealer { if self.seats.players_len() == 3 && self.box_cards.is_empty() { - if let Some(&card) = rng.choose_from(&self.deck) { + if let Some(card) = rng.choose_from(self.deck) { DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::PutInBox { card: Some(card) } })) } else { error!("Expected to deal a card but none were left in deck"); @@ -439,7 +439,7 @@ impl Game for Cribbage { State::Choosing => DealerAction::WaitForPlayer, State::TurnUp => { if let Some(username) = self.dealer { - if let Some(&card) = rng.choose_from(&self.deck) { + if let Some(card) = rng.choose_from(self.deck) { DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::CommunityCard { card } })) } else { error!("Expected to deal a card but none were left in deck"); diff --git a/src/game/poker/holdem.rs b/src/game/poker/holdem.rs index f0b8c47..a55a9ba 100644 --- a/src/game/poker/holdem.rs +++ b/src/game/poker/holdem.rs @@ -4,7 +4,7 @@ use std::convert::TryInto; use itertools::Itertools; -use crate::card::{Card, FIFTY_TWO_CARD_DECK}; +use crate::card::{Card, CardSet}; use crate::rng::{Seed, WaveRng}; use crate::seats::Seats; use crate::username::Username; @@ -102,9 +102,9 @@ pub struct TexasHoldEm { state: State, seats: Seats, stacks: HashMap, - deck: HashSet, - hands: HashMap>, - community: HashSet, + deck: CardSet, + hands: HashMap, + community: CardSet, dealer: Option, receiver: Option, active: Option, @@ -114,7 +114,7 @@ pub struct TexasHoldEm { pot: u64, level: Level, ghosts: HashMap, - revealed: HashSet<(Username, Card)>, + revealed: CardSet, } impl TexasHoldEm { @@ -130,9 +130,9 @@ impl TexasHoldEm { state: State::NotStarted, seats: Seats::new(), stacks: HashMap::new(), - deck: FIFTY_TWO_CARD_DECK.iter().cloned().collect(), + deck: CardSet::complete_deck(), hands: HashMap::new(), - community: HashSet::new(), + community: CardSet::new(), dealer: None, receiver: None, active: None, @@ -142,7 +142,7 @@ impl TexasHoldEm { pot: 0, level, ghosts: HashMap::new(), - revealed: HashSet::new(), + revealed: CardSet::new(), } } @@ -260,8 +260,8 @@ impl TexasHoldEm { if !self.settings.hide_cards && (self.players_able_to_bet() <= 1 || matches!(self.state, State::Showdown)) { for &username in self.in_hand.iter().sorted() { if let Some(hand) = self.hands.get(&username) { - for &card in hand.iter().sorted() { - if !self.revealed.contains(&(username, card)) { + for card in hand.into_iter().sorted() { + if !self.revealed.contains(&card) { return Some((username, card)); } } @@ -378,7 +378,7 @@ impl Game for TexasHoldEm { } self.remove_ghosts()?; self.dealer = Some(username); - self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect(); + self.deck = CardSet::complete_deck(); self.hands.clear(); self.community.clear(); self.active = None; @@ -405,7 +405,7 @@ impl Game for TexasHoldEm { Ok(()) } (_, Action::RevealCard { card }) => { - self.revealed.insert((username, card)); + self.revealed.insert(card); Ok(()) } (State::Dealing, Action::EndDeal) => { @@ -536,7 +536,7 @@ impl Game for TexasHoldEm { }, State::Dealing => { if let Some(username) = self.receiver { - let card = rng.choose_from(&self.deck).cloned(); + let card = rng.choose_from(self.deck); DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::ReceiveCard { card } })) } else if let Some(username) = self.dealer { DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::EndDeal })) @@ -630,7 +630,7 @@ impl Game for TexasHoldEm { if let Some((username, card)) = self.next_card_to_reveal() { DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::RevealCard { card } })) } else if let Some(username) = self.dealer { - if let Some(&card) = rng.choose_from(&self.deck) { + if let Some(card) = rng.choose_from(self.deck) { DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::CommunityCard { card } })) } else { error!("Dealing community card but there are no cards left in deck: {:#?}", self); @@ -670,7 +670,7 @@ impl Game for TexasHoldEm { .hands .iter() .sorted_by_key(|&(username, _)| username) - .map(|(&username, hand)| (username, hand.iter().chain(self.community.iter()).cloned().collect::>())) + .map(|(&username, hand)| (username, hand.into_iter().chain(self.community.into_iter()).collect::>())) .filter_map(|(username, cards)| cards.try_into().ok().map(rank_7_card_hand).map(|hand| (username, hand))) .max_items_by_key(|(_, hand)| *hand); info!("Showdown: community: {:?}", self.community); diff --git a/src/game/whist.rs b/src/game/whist.rs index 79a101e..38d77b9 100644 --- a/src/game/whist.rs +++ b/src/game/whist.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use crate::card::{Card, Suit, FIFTY_TWO_CARD_DECK}; +use crate::card::{Card, CardSet, Suit}; use crate::rng::{Seed, WaveRng}; use crate::seats::Seats; use crate::username::Username; @@ -44,8 +44,8 @@ pub struct KnockOutWhist { dealer: Option, receiver: Option, call: Option, - deck: HashSet, - hands: HashMap>, + deck: CardSet, + hands: HashMap, trick: HashMap, active: Option, tricks_won: HashMap, @@ -68,7 +68,7 @@ impl KnockOutWhist { dealer: None, receiver: None, call: None, - deck: FIFTY_TWO_CARD_DECK.iter().cloned().collect(), + deck: CardSet::complete_deck(), hands: HashMap::new(), trick: HashMap::new(), active: None, @@ -86,7 +86,7 @@ impl KnockOutWhist { } fn hand_contains_suit(&self, username: Username, suit: Suit) -> bool { - self.hands.get(&username).map_or(false, |hand| hand.iter().any(|card| card.suit == suit)) + self.hands.get(&username).map_or(false, |hand| hand.into_iter().any(|card| card.suit == suit)) } fn trick_winner(&self) -> Option { @@ -99,7 +99,7 @@ impl KnockOutWhist { } fn cards_by_player(&self) -> impl Iterator + '_ { - self.hands.iter().flat_map(|(username, hand)| hand.iter().map(move |card| (*username, *card))) + self.hands.iter().flat_map(|(username, hand)| hand.into_iter().map(move |card| (*username, card))) } fn all_hands_dealt(&self) -> bool { @@ -182,7 +182,7 @@ impl Game for KnockOutWhist { (State::NotStarted, Action::Leave) => self.seats.remove_player(username), (_, Action::NextToDeal) => { self.dealer = Some(username); - self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect(); + self.deck = CardSet::complete_deck(); self.hands.clear(); self.receiver = self.seats.player_after(username); self.trump_card = None; @@ -251,7 +251,7 @@ impl Game for KnockOutWhist { self.call = self.winners.iter().next().cloned(); } else { self.receiver = self.dealer.and_then(|dealer| self.seats.player_after_where(dealer, |username| self.winners.contains(&username))); - self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect(); + self.deck = CardSet::complete_deck(); self.hands.clear(); self.trump_card = None; self.state = State::CutForCall; @@ -277,7 +277,7 @@ impl Game for KnockOutWhist { (State::CutForCall, Action::CutCard { card }) => { self.deck.remove(&card); self.hands.entry(username).or_default().insert(card); - if self.hands.values().map(HashSet::len).sum::() == self.winners.len() { + if self.hands.values().map(CardSet::len).sum::() == self.winners.len() { if let Some((username, max)) = self.cards_by_player().max_by_key(|(_, card)| card.rank) { if self.cards_by_player().filter(|(_, card)| card.rank == max.rank).count() > 1 { self.winners = self.cards_by_player().filter(|(_, card)| card.rank == max.rank).map(|(username, _)| username).collect(); @@ -333,12 +333,12 @@ impl Game for KnockOutWhist { }, State::Dealing => { if let Some(username) = self.receiver { - let card = rng.choose_from(&self.deck).cloned(); + let card = rng.choose_from(self.deck); DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::ReceiveCard { card } })) } else if let Some(username) = self.dealer { match (self.call, self.trump_card) { (None, None) => { - if let Some(&card) = rng.choose_from(&self.deck) { + if let Some(card) = rng.choose_from(self.deck) { DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::CommunityCard { card } })) } else { error!("Expected to deal a card but none were left in deck"); @@ -379,7 +379,7 @@ impl Game for KnockOutWhist { } State::CutForCall => { if let Some(username) = self.receiver { - if let Some(card) = rng.choose_from(&self.deck).cloned() { + if let Some(card) = rng.choose_from(self.deck) { DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::CutCard { card } })) } else { error!("Expected to cut for call but there were no cards left in the deck");