Last active
August 29, 2015 14:25
-
-
Save petertseng/184e82bed2200ecefc47 to your computer and use it in GitHub Desktop.
poker hands in rust and ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Card | |
attr_reader :rank, :suit | |
def initialize(rank, suit) | |
@rank = rank | |
@suit = suit | |
end | |
def to_s | |
"#{@rank} of #{@suit}" | |
end | |
end | |
def order(hand) | |
case hand | |
when :five_of_a_kind; 100 | |
when :royal_flush; 90 | |
when :straight_flush; 80 | |
when :four_of_a_kind; 70 | |
when :full_house; 60 | |
when :flush; 50 | |
when :straight; 40 | |
when :three_of_a_kind; 30 | |
when :two_pair; 20 | |
when :pair; 10 | |
when :nothing; 0 | |
else; raise "Unknown hand #{hand}" | |
end | |
end | |
def eval_hand(ranks, num_wilds, flush) | |
rank_freqs = ranks.each_with_object(Hash.new(0)) { |rank, freq| freq[rank] += 1 } | |
count_freqs = rank_freqs.each_with_object(Hash.new(0)) { |(_, value), freq| freq[value] += 1 } | |
ranks_distinct = count_freqs[1] == ranks.size | |
if num_wilds == 0 | |
royal_straight = ranks == [1, 10, 11, 12, 13] | |
straight = ranks_distinct && ranks.last - ranks.first == 4 | |
else | |
# Ranks are all distinct, and wild cards can make up all the remaining cards needed. | |
royal_straight = ranks_distinct && ([1, 10, 11, 12, 13] - ranks).size == num_wilds | |
gap = ranks.last - ranks.first - 1 | |
# Straight with wilds outside: the ranked cards just need to be consecutive. | |
# That means ranks.last - ranks.first == ranks.size - 1 | |
# Or to use the gap variable, gap == ranks.size - 2 | |
# Straight with wilds inside: the ranked cards are all distinct, | |
# and the wild cards can fill the gap (gap <= 3). | |
# (there may be wild s outside too, that's fine!) | |
# If gap > 3 (for example, 7 to 2 has 4 gap) we can't make a straight. | |
straight = ranks_distinct && (gap <= 3 || gap == ranks.size - 2) | |
end | |
return :five_of_a_kind if count_freqs[5 - num_wilds] > 0 | |
return :royal_flush if royal_straight && flush | |
return :straight_flush if straight && flush | |
return :four_of_a_kind if count_freqs[4 - num_wilds] > 0 | |
# Won't happen with >= 2 wilds (would become 4 of a kind). | |
return :full_house if count_freqs[3] > 0 && count_freqs[2] > 0 | |
return :full_house if count_freqs[2] == 2 && num_wilds == 1 | |
return :flush if flush | |
return :straight if straight || royal_straight | |
return :three_of_a_kind if count_freqs[3 - num_wilds] > 0 | |
# Won't happen with any wilds (would become 3 of a kind). | |
return :two_pair if count_freqs[2] == 2 | |
return :pair if count_freqs[2 - num_wilds] > 0 | |
:nothing | |
end | |
class FreqInfo | |
attr_reader :total | |
def initialize | |
@by_wilds = [0, 0, 0, 0, 0] | |
@total = 0 | |
end | |
def incr(wilds) | |
@by_wilds[wilds] += 1 | |
@total += 1 | |
end | |
def [](x) | |
@by_wilds[x] | |
end | |
end | |
class Counter | |
attr_reader :num_wilds | |
attr_reader :cache_misses | |
attr_reader :cache_hits | |
def initialize(wild_rank, jokers, flushes_allowed) | |
@cache_misses = 0 | |
@cache_hits = 0 | |
@cache = {true => {}, false => {}} | |
@flushes_allowed = flushes_allowed | |
@jokers = jokers | |
@wild_rank = wild_rank | |
@num_wilds = wild_rank ? (wild_rank == :joker ? jokers : 4) : 0 | |
end | |
def count | |
suits = [:spades, :clubs, :diamonds, :hearts] | |
deck = (1..13).flat_map { |rank| suits.map { |suit| Card.new(rank, suit) }} | |
@jokers.times { deck << Card.new(:joker, :joker) } | |
deck.combination(5).each_with_object(Hash.new { |h, k| h[k] = FreqInfo.new }) { |cards, freq| | |
if @wild_rank | |
wilds, non_wilds = cards.partition { |c| c.rank == @wild_rank } | |
num_wilds = wilds.size | |
else | |
num_wilds, non_wilds = 0, cards | |
end | |
ranks = non_wilds.map(&:rank) | |
if @flushes_allowed | |
first_suit = non_wilds.first.suit | |
flush = non_wilds.all? { |nw| nw.suit == first_suit } | |
else | |
flush = false | |
end | |
cache_key = ranks.inject(0) { |key, rank| key * 14 + rank } | |
if (hand = @cache[flush][cache_key]) | |
@cache_hits += 1 | |
else | |
hand = eval_hand(ranks, num_wilds, flush) | |
@cache[flush][cache_key] = hand | |
@cache_misses += 1 | |
end | |
freq[hand].incr(num_wilds) | |
} | |
end | |
end | |
counter = Counter.new(nil, 0, true) | |
counter.count.to_a.sort_by { |rank, info| -order(rank) }.each { |rank, info| | |
cells = (0..counter.num_wilds).map { |wilds| '%7d' % info[wilds] } | |
puts ('%18s ' % rank) + cells.join(' ') + (' %7d' % info.total) | |
} | |
puts counter.cache_hits | |
puts counter.cache_misses |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::collections::BTreeMap; | |
use std::collections::HashMap; | |
use std::fmt::{Display, Error, Formatter}; | |
#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] | |
enum Hand { | |
FiveOfAKind, | |
RoyalFlush, | |
StraightFlush, | |
FourOfAKind, | |
FullHouse, | |
Flush, | |
Straight, | |
ThreeOfAKind, | |
TwoPair, | |
Pair, | |
HighCard, | |
} | |
impl Display for Hand { | |
fn fmt(&self, format: &mut Formatter) -> Result<(), Error> { | |
let string = match *self { | |
Hand::FiveOfAKind => "Five of a Kind", | |
Hand::RoyalFlush => "Royal Flush", | |
Hand::StraightFlush => "Straight Flush", | |
Hand::FourOfAKind => "Four of a Kind", | |
Hand::FullHouse => "Full House", | |
Hand::Flush => "Flush", | |
Hand::Straight => "Straight", | |
Hand::ThreeOfAKind => "Three of a Kind", | |
Hand::TwoPair => "Two Pair", | |
Hand::Pair => "Pair", | |
Hand::HighCard => "High Card", | |
}; | |
format.pad(string) | |
} | |
} | |
type Rank = u8; | |
#[derive(Copy, Clone, Eq, PartialEq)] | |
enum Suit { | |
Clubs, | |
Diamonds, | |
Hearts, | |
Spades, | |
Joker, | |
} | |
#[derive(Copy, Clone)] | |
struct Card { | |
suit: Suit, | |
rank: Rank, | |
} | |
struct HandFrequency { | |
by_num_wilds: Vec<usize>, | |
total: usize, | |
} | |
impl HandFrequency { | |
fn new() -> HandFrequency { | |
HandFrequency { | |
by_num_wilds: vec![0, 0, 0, 0, 0], | |
total: 0, | |
} | |
} | |
fn add(&mut self, num_wilds: usize) { | |
self.by_num_wilds[num_wilds] += 1; | |
self.total += 1; | |
} | |
} | |
type HandKey = u32; | |
struct HandCounter { | |
freqs: BTreeMap<Hand, HandFrequency>, | |
cache: HashMap<HandKey, Hand>, | |
flush_allowed: bool, | |
wild_rank: Option<Rank>, | |
} | |
impl HandCounter { | |
fn new() -> HandCounter { | |
HandCounter { | |
// TODO: Jokers wild | |
freqs: BTreeMap::new(), | |
cache: HashMap::new(), | |
flush_allowed: true, | |
wild_rank: None, | |
} | |
} | |
fn num_wilds(&self) -> usize { | |
match self.wild_rank { | |
// TODO: Jokers wild | |
Some(_) => 4, | |
None => 0, | |
} | |
} | |
fn count(count_ranks: &HashMap<usize, usize>, n: usize) -> usize { | |
count_ranks.get(&n).map_or(0, |x| *x) | |
} | |
fn eval_hand(ranks: Vec<Rank>, num_wilds: usize, flush: bool) -> Hand { | |
// How many of each rank are there? | |
let mut rank_freqs = HashMap::new(); | |
for rank in &ranks { | |
*rank_freqs.entry(*rank).or_insert(0) += 1; | |
} | |
// How many N-of-a-kinds do we have? | |
let mut count_ranks = HashMap::new(); | |
for count in rank_freqs.values() { | |
*count_ranks.entry(*count).or_insert(0) += 1; | |
} | |
let ranks_distinct = count_ranks.get(&1).map_or(false, |x| *x == ranks.len()); | |
let royal_straight = if num_wilds == 0 { | |
ranks == vec![1, 10, 11, 12, 13] | |
} else { | |
ranks_distinct && ranks.iter().all(|x| *x == 1 || *x >= 10) | |
}; | |
let straight = if num_wilds == 0 { | |
ranks_distinct && ranks[4] - ranks[0] == 4 | |
} else { | |
let gap = ranks[ranks.len() - 1] - ranks[0] - 1; | |
// Straight with wilds outside: The ranked cards must be consecutive. | |
// That means ranks.last - ranks.first == ranks.size - 1 | |
// Straight with wilds inside: The wild cards can fill the gap. | |
// There may be wilds outside too, that's fine. | |
// If gap > 3 (for example, 7 to 2 has 4 gap) we can't make a straight. | |
ranks_distinct && (gap <= 3 || gap == ((ranks.len() - 2) as Rank)) | |
}; | |
if HandCounter::count(&count_ranks, 5 - num_wilds) > 0 { return Hand::FiveOfAKind } | |
if royal_straight && flush { return Hand::RoyalFlush } | |
if straight && flush { return Hand::StraightFlush } | |
if HandCounter::count(&count_ranks, 4 - num_wilds) > 0 { return Hand::FourOfAKind } | |
// Won't happen with >= 2 wilds (would become 4 of a kind). | |
if HandCounter::count(&count_ranks, 3) > 0 && HandCounter::count(&count_ranks, 2) > 0 { return Hand::FullHouse; } | |
if HandCounter::count(&count_ranks, 2) == 2 && num_wilds == 1 { return Hand::FullHouse; } | |
if flush { return Hand::Flush } | |
if straight || royal_straight { return Hand::Straight } | |
if HandCounter::count(&count_ranks, 3 - num_wilds) > 0 { return Hand::ThreeOfAKind } | |
// Won't happen with any wilds (would become 3 of a kind) | |
if HandCounter::count(&count_ranks, 2) == 2 { return Hand::TwoPair } | |
if HandCounter::count(&count_ranks, 2 - num_wilds) > 0 { return Hand::Pair } | |
Hand::HighCard | |
} | |
fn add_hand(&mut self, hand: [&Card; 5]) { | |
let (num_wilds, non_wilds) = match self.wild_rank { | |
Some(x) => { | |
// No partition(), I don't care what the wilds are. | |
let mut non_wilds = Vec::new(); | |
let mut num_wilds = 0; | |
for card in hand.iter() { | |
if card.rank == x { | |
num_wilds += 1; | |
} else { | |
non_wilds.push(card); | |
} | |
} | |
(num_wilds, non_wilds) | |
}, | |
None => (0, hand.iter().collect()), | |
}; | |
let flush = self.flush_allowed && non_wilds.iter().all(|card| card.suit == non_wilds[0].suit); | |
let hand_key = (hand[0].rank as HandKey) | (hand[1].rank as HandKey) << 4 | (hand[2].rank as HandKey) << 8 | (hand[3].rank as HandKey) << 12 | (hand[4].rank as HandKey) << 16 | if flush { 1 << 20 } else { 0 }; | |
let hand_type = *self.cache.entry(hand_key).or_insert_with(|| HandCounter::eval_hand(non_wilds.iter().map(|card| card.rank).collect(), num_wilds, flush)); | |
self.freqs.entry(hand_type).or_insert(HandFrequency::new()).add(num_wilds); | |
} | |
} | |
fn main() { | |
let cards: Vec<_> = (1..14).flat_map(|i| vec![ | |
Card {suit: Suit::Clubs, rank: i}, | |
Card {suit: Suit::Diamonds, rank: i}, | |
Card {suit: Suit::Hearts, rank: i}, | |
Card {suit: Suit::Spades, rank: i}, | |
].into_iter()).collect(); | |
let mut hc = HandCounter::new(); | |
// Interesting... | |
for index0 in 0..cards.len() { | |
for index1 in (index0 + 1)..cards.len() { | |
for index2 in (index1 + 1)..cards.len() { | |
for index3 in (index2 + 1)..cards.len() { | |
for index4 in (index3 + 1)..cards.len() { | |
hc.add_hand([&cards[index0], &cards[index1], &cards[index2], &cards[index3], &cards[index4]]) | |
} | |
} | |
} | |
} | |
} | |
let num_wilds = hc.num_wilds(); | |
for (hand, frequency) in hc.freqs.iter() { | |
print!("{: >15}:", hand); | |
if num_wilds > 0 { | |
for wilds in 0..(num_wilds + 1) { | |
print!(" {:>7}", frequency.by_num_wilds[wilds]); | |
} | |
} | |
println!(" {:>7}", frequency.total); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment