Poker hands
Poker hands of 5 to 7 cards evaluated and named, showdowns with split pots and kickers, Omaha's rule of exactly two from the hand and three from the board, equity counted exactly on the flop and turn and simulated before it from a seed, and decks dealt from a seed.
Version 1.0.0 · 7 functions · 1 crate
Functions
What use poker.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use poker_evaluate, poker_omaha from poker (Quillo 0.6).
poker_evaluate(hand: list) → anyor an errorThe best five of 5 to 7 cards: { category, rank, value, name, best }. value is higher for a better hand and equal for a tie. name reads "full house, kings over threes". Aces play low in the wheel, A-2-3-4-5.
poker_omaha(hole: list, board: list) → anyor an errorThe best Omaha hand: exactly two of the 4 to 6 hole cards with exactly three of the 3 to 5 board cards, so four spades on the board with one in the hand is no flush.
poker_showdown(hands: any, board: list, options: any) → anyor an errorHands compared at showdown: { winners, hands }. winners holds positions from 0, more than one on a split pot. board may be [] with hands of five to seven. options: { game: "holdem" | "omaha" }.
poker_equity(hands: any, board: list, options: any) → anyor an errorEach hand's share of the pot: { method, iterations, hands: [{ win, tie, equity }] }. With at most two board cards to come it counts every runout exactly; otherwise it simulates iterations boards (default 20000) from seed, the same each time. options: { game, seed, iterations, dead }. tie is the share of pots split, weighted by the split.
poker_deck(seed: int) → listA shuffled deck from a seed: 52 cards, the same each time, by a Fisher-Yates shuffle.
poker_deal(players: int, game: str, seed: int) → anyor an errorA deal from a seed: { hands, board, burned }. game is holdem or omaha; the deck is dealt round the table, one card at a time, burning before the flop, turn and river.
poker_hand_class(hole: list) → stror an errorTwo hole cards as a starting-hand class: AKs, AKo or QQ, the shorthand hand charts use.
Get it
$ quillo install poker # poker.qwer into this folder$ quillo install poker --example # and its example, poker.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/poker.qwer$ curl -O https://quillolang.org/plugins/files/poker.qweThen, in your program, all of it or only what you call:
use poker.qweruse poker_evaluate, poker_omaha from poker // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// Poker: hands named and compared, a split pot, Omaha's rule that catches a beginner out, the// chance each hand has, and a seeded deal.//// quillo run poker.qwe// then open http://localhost:8080/hand?cards=As,Ks,Qs,Js,Ts, /ranks, /showdown, /omaha, /equity and /dealuse "poker.qwer"route GET /hand ? { cards: str } { let r = poker_evaluate(split(cards, ",")) if is_err(r) { reply 422 r.error } r}route GET /ranks = { royal: poker_evaluate(["As", "Ks", "Qs", "Js", "Ts"])?.name, steel_wheel: poker_evaluate(["5d", "4d", "3d", "2d", "Ad"])?.name, four: poker_evaluate(["9c", "9d", "9h", "9s", "Kd"])?.name, boat: poker_evaluate(["Kc", "Kd", "Kh", "3s", "3d"])?.name, flush: poker_evaluate(["Ah", "Jh", "9h", "4h", "2h"])?.name, wheel: poker_evaluate(["Ac", "2d", "3h", "4s", "5c"])?.name, trips: poker_evaluate(["7c", "7d", "7h", "2s", "Kd"])?.name, two_pair: poker_evaluate(["Jc", "Jd", "4h", "4s", "Ad"])?.name, pair: poker_evaluate(["10c", "10d", "4h", "8s", "Ad"])?.name, high: poker_evaluate(["Kc", "Jd", "4h", "8s", "2d"])?.name, seven: poker_evaluate(["Ah", "Kh", "Qh", "Jh", "2c", "3d", "Th"])?, symbols: poker_evaluate(["A♠", "K♠", "Q♠", "J♠", "♠9"])?.category,}route GET /showdown = { won: poker_showdown([["Ah", "Kd"], ["Qs", "Qc"]], ["2c", "7d", "9h", "Js", "3s"], {})?, split: poker_showdown([["Ah", "2d"], ["Ad", "3c"]], ["Ks", "Qs", "Js", "Ts", "5h"], {})?.winners, kicker: poker_showdown([["Ah", "Kd"], ["As", "Qc"]], ["Ad", "7d", "9h", "4s", "3c"], {})?.winners, five_card: poker_showdown([["Ah", "Ad", "Kc", "Kd", "2s"], ["Qh", "Qd", "Qc", "4d", "5s"]], [], {})?.winners,}// Four spades on the board, one in the hand: no flush in Omaha, a flush in hold'em.route GET /omaha = { omaha: poker_omaha(["As", "Kd", "Qc", "2h"], ["Ks", "9s", "5s", "3s", "7d"])?.name, holdem: poker_evaluate(["As", "Kd", "Ks", "9s", "5s", "3s", "7d"])?.name, showdown: poker_showdown([["As", "Kd", "Qc", "2h"], ["Td", "9d", "7c", "6h"]], ["Ks", "9s", "5s", "3s", "8d"], { game: "omaha" })?.winners, too_few: poker_omaha(["As", "Kd"], ["Ks", "9s", "5s"]).error,}route GET /equity = { preflop: poker_equity([["As", "Ah"], ["Ks", "Kh"]], [], { seed: 42, iterations: 50000 })?, same_seed: poker_equity([["As", "Ah"], ["Ks", "Kh"]], [], { seed: 42, iterations: 500 })?.hands == poker_equity([["As", "Ah"], ["Ks", "Kh"]], [], { seed: 42, iterations: 500 })?.hands, flop: poker_equity([["Ah", "Kh"], ["Qs", "Qd"]], ["2h", "7h", "Qc"], {})?, river: poker_equity([["Ah", "2d"], ["Ad", "3c"]], ["Ks", "Qs", "Js", "Ts", "5h"], {})?, dead: poker_equity([["Ah", "Kh"], ["Qs", "Qd"]], ["2h", "7h", "Qc"], { dead: ["3h", "4h", "5h", "6h", "8h", "9h", "Th", "Jh", "Qh"] })?.hands, omaha: poker_equity([["As", "Ks", "Qd", "Jd"], ["2c", "2d", "7h", "8h"]], ["Ah", "3c", "9s"], { game: "omaha" })?.method,}route GET /deal { let d = poker_deal(3, "holdem", 7)? { deal: d, same: poker_deal(3, "holdem", 7)? == d, deck: len(unique(poker_deck(7))), class: [poker_hand_class(["Kd", "As"])?, poker_hand_class(["Qh", "Jh"])?, poker_hand_class(["9c", "9d"])?], }}route GET /refused = { card: poker_evaluate(["Zz", "Ks", "Qs", "Js", "Ts"]).error, twice: poker_evaluate(["As", "As", "Qs", "Js", "Ts"]).error, count: poker_evaluate(["As", "Ks"]).error, shared: poker_showdown([["Ah", "Kd"], ["Ah", "Qc"]], ["2c", "7d", "9h", "Js", "3s"], {}).error, game: poker_showdown([["Ah", "Kd"], ["Qh", "Qc"]], [], { game: "razz" }).error,}$ quillo run poker.qwe$ curl 'localhost:8080/hand?cards=As,Ks,Qs,Js,Ts'$ curl 'localhost:8080/ranks'$ curl 'localhost:8080/showdown'$ curl 'localhost:8080/omaha'$ curl 'localhost:8080/equity'$ curl 'localhost:8080/deal'$ curl 'localhost:8080/refused'Source
//! Poker hands evaluated and compared for 5 to 7 cards, Texas hold'em showdowns and Omaha's rule of two from the hand and three from the board, equity by simulation with a seed or exactly when few cards are left, and decks dealt from a seed.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};const RANKS: &[u8; 13] = b"23456789TJQKA";const SUITS: &[u8; 4] = b"cdhs";const WORDS: [&str; 13] = ["two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "jack", "queen", "king", "ace"];const PLURAL: [&str; 13] = ["twos", "threes", "fours", "fives", "sixes", "sevens", "eights", "nines", "tens", "jacks", "queens", "kings", "aces"];const CATEGORIES: [&str; 9] = ["high card", "pair", "two pair", "three of a kind", "straight", "flush", "full house", "four of a kind", "straight flush"];// A card is rank * 4 + suit.type Card = u8;fn card(s: &str) -> Result<Card, String> { let t = s.trim(); let mut c: Vec<char> = t.chars().collect(); if c.len() == 3 && c[0] == '1' && c[1] == '0' { c = vec!['T', c[2]]; } if c.len() != 2 { return Err(format!("`{}` is not a card: like As, Td, 10h, 2c", s)); } // Either order, and the suit symbols. let suit_of = |x: char| match x { 'c' | 'C' | '♣' | '♧' => Some(0u8), 'd' | 'D' | '♦' | '♢' => Some(1), 'h' | 'H' | '♥' | '♡' => Some(2), 's' | 'S' | '♠' | '♤' => Some(3), _ => None, }; let (r, su) = match (suit_of(c[1]), suit_of(c[0])) { (Some(x), _) => (c[0], x), (None, Some(x)) => (c[1], x), _ => return Err(format!("`{}` has no suit: c, d, h or s", s)), }; let rank = RANKS.iter().position(|x| *x as char == r.to_ascii_uppercase()).ok_or(format!("`{}` has no rank: 2 to 9, T, J, Q, K, A", s))? as u8; Ok(rank * 4 + su)}fn name(c: Card) -> String { format!("{}{}", RANKS[(c / 4) as usize] as char, SUITS[(c % 4) as usize] as char)}fn cards(list: &[String]) -> Result<Vec<Card>, String> { list.iter().map(|s| card(s)).collect()}fn no_doubles(groups: &[&[Card]]) -> Result<(), String> { let mut seen = 0u64; for g in groups { for c in *g { if seen >> c & 1 == 1 { return Err(format!("{} is dealt twice", name(*c))); } seen |= 1 << c; } } Ok(())}// Category in the top bits, then the deciding ranks in order: higher is better, and equal is a tie.fn eval5(h: &[Card]) -> u32 { let mut counts = [0u8; 13]; let mut flush = true; let mut mask = 0u16; for c in h { counts[(c / 4) as usize] += 1; mask |= 1 << (c / 4); flush &= c % 4 == h[0] % 4; } let mut groups: Vec<(u8, u8)> = (0..13u8).rev().filter(|r| counts[*r as usize] > 0).map(|r| (counts[r as usize], r)).collect(); groups.sort_by(|a, b| b.cmp(a)); let straight_high = if groups.len() == 5 { if mask == 0b1_0000_0000_1111 { Some(3u8) // The wheel, A-2-3-4-5, is five high. } else { let top = 15 - mask.leading_zeros() as u8; if mask >> (top - 4) == 0b11111 { Some(top) } else { None } } } else { None }; let packed = |cat: u32, rs: &[u8]| -> u32 { let mut v = cat; for i in 0..5 { v = v << 4 | *rs.get(i).unwrap_or(&0) as u32; } v }; let rs: Vec<u8> = groups.iter().map(|g| g.1).collect(); match (flush, straight_high, groups[0].0, groups.get(1).map(|g| g.0).unwrap_or(0)) { (true, Some(t), _, _) => packed(8, &[t]), (_, _, 4, _) => packed(7, &rs), (_, _, 3, 2) => packed(6, &rs), (true, None, _, _) => packed(5, &rs), (false, Some(t), _, _) => packed(4, &[t]), (_, _, 3, _) => packed(3, &rs), (_, _, 2, 2) => packed(2, &rs), (_, _, 2, _) => packed(1, &rs), _ => packed(0, &rs), }}fn best_of(cs: &[Card]) -> (u32, [Card; 5]) { let n = cs.len(); let mut best = (0u32, [0; 5]); for a in 0..n { for b in a + 1..n { for c in b + 1..n { for d in c + 1..n { for e in d + 1..n { let h = [cs[a], cs[b], cs[c], cs[d], cs[e]]; let v = eval5(&h); if v > best.0 { best = (v, h); } } } } } } best}// Omaha: exactly two of the hole cards and exactly three of the board.fn best_omaha(hole: &[Card], board: &[Card]) -> (u32, [Card; 5]) { let mut best = (0u32, [0; 5]); for a in 0..hole.len() { for b in a + 1..hole.len() { for c in 0..board.len() { for d in c + 1..board.len() { for e in d + 1..board.len() { let h = [hole[a], hole[b], board[c], board[d], board[e]]; let v = eval5(&h); if v > best.0 { best = (v, h); } } } } } } best}fn describe(v: u32) -> String { let cat = v >> 20; let r = |i: u32| (v >> (16 - i * 4) & 15) as usize; match cat { 8 if r(0) == 12 => "royal flush".to_string(), 8 => format!("straight flush, {} high", WORDS[r(0)]), 7 => format!("four {}, {} kicker", PLURAL[r(0)], WORDS[r(1)]), 6 => format!("full house, {} over {}", PLURAL[r(0)], PLURAL[r(1)]), 5 => format!("flush, {} high", WORDS[r(0)]), 4 => format!("straight, {} high", WORDS[r(0)]), 3 => format!("three {}", PLURAL[r(0)]), 2 => format!("two pair, {} and {}", PLURAL[r(0)], PLURAL[r(1)]), 1 => format!("pair of {}", PLURAL[r(0)]), _ => format!("{} high", WORDS[r(0)]), }}fn result(v: u32, best: &[Card; 5]) -> Value { let mut b: Vec<Card> = best.to_vec(); b.sort_by(|a, c| c.cmp(a)); json!({ "category": CATEGORIES[(v >> 20) as usize], "rank": v >> 20, "value": v, "name": describe(v), "best": b.iter().map(|c| name(*c)).collect::<Vec<_>>() })}/// The best five of 5 to 7 cards: { category, rank, value, name, best }. `value` is higher for a better hand and equal for a tie. `name` reads "full house, kings over threes". Aces play low in the wheel, A-2-3-4-5.pub fn poker_evaluate(hand: Vec<String>) -> Result<Value, String> { let c = cards(&hand)?; if !(5..=7).contains(&c.len()) { return Err(format!("a hand here is 5 to 7 cards, not {}", c.len())); } no_doubles(&[&c])?; let (v, b) = best_of(&c); Ok(result(v, &b))}/// The best Omaha hand: exactly two of the 4 to 6 hole cards with exactly three of the 3 to 5 board cards, so four spades on the board with one in the hand is no flush.pub fn poker_omaha(hole: Vec<String>, board: Vec<String>) -> Result<Value, String> { let (h, b) = (cards(&hole)?, cards(&board)?); if !(4..=6).contains(&h.len()) { return Err(format!("Omaha has 4 to 6 hole cards, not {}", h.len())); } if !(3..=5).contains(&b.len()) { return Err(format!("the board has 3 to 5 cards, not {}", b.len())); } no_doubles(&[&h, &b])?; let (v, best) = best_omaha(&h, &b); Ok(result(v, &best))}fn holes(hands: &Value) -> Result<Vec<Vec<Card>>, String> { let list = hands.as_array().ok_or("give the hands as a list of lists of cards")?; if list.len() < 2 { return Err("a showdown has at least two hands".into()); } list.iter() .map(|h| { let l = h.as_array().ok_or("each hand is a list of cards")?; l.iter().map(|c| card(c.as_str().unwrap_or(""))).collect::<Result<Vec<_>, _>>() }) .collect()}fn game_of(options: &Value) -> Result<bool, String> { match options.get("game").and_then(|v| v.as_str()).unwrap_or("holdem") { "holdem" | "hold'em" | "texas" => Ok(false), "omaha" | "plo" => Ok(true), o => Err(format!("no game `{}`; there are holdem and omaha", o)), }}/// Hands compared at showdown: { winners, hands }. `winners` holds positions from 0, more than one on a split pot. `board` may be [] with hands of five to seven. `options`: { game: "holdem" | "omaha" }.pub fn poker_showdown(hands: serde_json::Value, board: Vec<String>, options: serde_json::Value) -> Result<Value, String> { let omaha = game_of(&options)?; let hs = holes(&hands)?; let b = cards(&board)?; let all: Vec<&[Card]> = hs.iter().map(|h| h.as_slice()).chain([b.as_slice()]).collect(); no_doubles(&all)?; let mut rows = vec![]; let mut best = 0; for h in &hs { let r = if omaha { if h.len() < 4 || b.len() < 3 { return Err("Omaha needs four hole cards and at least three on the board".into()); } best_omaha(h, &b) } else { let mut seven = h.clone(); seven.extend_from_slice(&b); if seven.len() < 5 { return Err(format!("{} cards make no hand", seven.len())); } best_of(&seven) }; best = best.max(r.0); rows.push(r); } let winners: Vec<usize> = rows.iter().enumerate().filter(|(_, r)| r.0 == best).map(|(i, _)| i).collect(); Ok(json!({ "winners": winners, "hands": rows.iter().map(|r| result(r.0, &r.1)).collect::<Vec<_>>() }))}struct Rng(u64);impl Rng { fn next(&mut self) -> u64 { self.0 = self.0.wrapping_add(0x9E3779B97F4A7C15); let mut z = self.0; z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB); z ^ (z >> 31) } fn below(&mut self, n: usize) -> usize { let n = n as u64; let zone = u64::MAX - u64::MAX % n; loop { let x = self.next(); if x < zone { return (x % n) as usize; } } }}fn tally(hs: &[Vec<Card>], board: &[Card], omaha: bool, win: &mut [f64], tie: &mut [f64]) { let mut scores = [0u32; 10]; let mut best = 0; for (i, h) in hs.iter().enumerate() { let v = if omaha { best_omaha(h, board).0 } else { let mut all: Vec<Card> = h.clone(); all.extend_from_slice(board); best_of(&all).0 }; scores[i] = v; best = best.max(v); } let n = scores[..hs.len()].iter().filter(|v| **v == best).count(); for i in 0..hs.len() { if scores[i] == best { if n == 1 { win[i] += 1.0; } else { tie[i] += 1.0 / n as f64; } } }}/// Each hand's share of the pot: { method, iterations, hands: [{ win, tie, equity }] }. With at most two board cards to come it counts every runout exactly; otherwise it simulates `iterations` boards (default 20000) from `seed`, the same each time. `options`: { game, seed, iterations, dead }. `tie` is the share of pots split, weighted by the split.pub fn poker_equity(hands: serde_json::Value, board: Vec<String>, options: serde_json::Value) -> Result<Value, String> { let omaha = game_of(&options)?; let hs = holes(&hands)?; if hs.len() > 10 { return Err("at most ten hands".into()); } let b = cards(&board)?; let dead: Vec<Card> = match options.get("dead").and_then(|v| v.as_array()) { Some(d) => d.iter().map(|c| card(c.as_str().unwrap_or(""))).collect::<Result<_, _>>()?, None => vec![], }; if b.len() > 5 { return Err("the board has at most five cards".into()); } for h in &hs { if omaha && !(4..=6).contains(&h.len()) { return Err(format!("Omaha has 4 to 6 hole cards, not {}", h.len())); } if !omaha && h.len() != 2 { return Err(format!("hold'em has 2 hole cards, not {}", h.len())); } } let mut all: Vec<&[Card]> = hs.iter().map(|h| h.as_slice()).collect(); all.push(&b); all.push(&dead); no_doubles(&all)?; let used: u64 = all.iter().flat_map(|g| g.iter()).fold(0, |a, c| a | 1 << c); let deck: Vec<Card> = (0..52).filter(|c| used >> c & 1 == 0).collect(); let need = 5 - b.len(); let (mut win, mut tie) = (vec![0.0; hs.len()], vec![0.0; hs.len()]); let mut count = 0u64; let exact = need <= 2; if exact { let mut run = b.clone(); run.resize(5, 0); match need { 0 => { tally(&hs, &run, omaha, &mut win, &mut tie); count = 1; } 1 => { for c in &deck { run[4] = *c; tally(&hs, &run, omaha, &mut win, &mut tie); count += 1; } } _ => { for i in 0..deck.len() { for j in i + 1..deck.len() { run[3] = deck[i]; run[4] = deck[j]; tally(&hs, &run, omaha, &mut win, &mut tie); count += 1; } } } } } else { let n = options.get("iterations").and_then(|v| v.as_i64()).unwrap_or(20000); if !(1..=5_000_000).contains(&n) { return Err(format!("iterations is 1 to 5000000, not {}", n)); } let mut rng = Rng(options.get("seed").and_then(|v| v.as_i64()).unwrap_or(1) as u64); let mut d = deck.clone(); let mut run = b.clone(); run.resize(5, 0); for _ in 0..n { // A partial Fisher-Yates: only the cards needed are drawn. for k in 0..need { let j = k + rng.below(d.len() - k); d.swap(k, j); run[b.len() + k] = d[k]; } tally(&hs, &run, omaha, &mut win, &mut tie); count += 1; } } let share = |x: f64| (x / count as f64 * 10000.0).round() / 10000.0; Ok(json!({ "method": if exact { "exact" } else { "simulation" }, "iterations": count, "hands": (0..hs.len()).map(|i| json!({ "win": share(win[i]), "tie": share(tie[i]), "equity": share(win[i] + tie[i]) })).collect::<Vec<_>>(), }))}/// A shuffled deck from a seed: 52 cards, the same each time, by a Fisher-Yates shuffle.pub fn poker_deck(seed: i64) -> Vec<String> { let mut d: Vec<Card> = (0..52).collect(); let mut r = Rng(seed as u64); for i in (1..52).rev() { d.swap(i, r.below(i + 1)); } d.iter().map(|c| name(*c)).collect()}/// A deal from a seed: { hands, board, burned }. `game` is holdem or omaha; the deck is dealt round the table, one card at a time, burning before the flop, turn and river.pub fn poker_deal(players: i64, game: &str, seed: i64) -> Result<Value, String> { let per = match game { "holdem" => 2, "omaha" => 4, o => return Err(format!("no game `{}`; there are holdem and omaha", o)), }; if !(2..=10).contains(&players) { return Err(format!("2 to 10 players, not {}", players)); } let d = poker_deck(seed); let p = players as usize; let hands: Vec<Vec<String>> = (0..p).map(|i| (0..per).map(|r| d[r * p + i].clone()).collect()).collect(); let at = p * per; Ok(json!({ "hands": hands, "board": [&d[at + 1], &d[at + 2], &d[at + 3], &d[at + 5], &d[at + 7]], "burned": [&d[at], &d[at + 4], &d[at + 6]], }))}/// Two hole cards as a starting-hand class: AKs, AKo or QQ, the shorthand hand charts use.pub fn poker_hand_class(hole: Vec<String>) -> Result<String, String> { let c = cards(&hole)?; if c.len() != 2 { return Err("a starting hand is two cards".into()); } no_doubles(&[&c])?; let (a, b) = (c[0].max(c[1]), c[0].min(c[1])); let (hi, lo) = (RANKS[(a / 4) as usize] as char, RANKS[(b / 4) as usize] as char); Ok(if a / 4 == b / 4 { format!("{}{}", hi, lo) } else { format!("{}{}{}", hi, lo, if a % 4 == b % 4 { 's' } else { 'o' }) })}Related
dice.qwer · 1.0.0Dice notation parsed and rolled from a seed: `4d6kh3`, `d20adv`, exploding and compounding dice, rerolls, success counts, Fudge dice and arithmetic, each die shown, and the exact distribution of an expression worked out by convolution rather than sampled, with the chance of meeting a target.4 views · 11 downloadsGo game records go_sgf.qwer · 1.0.0SGF game records read and written with their variations, escapes and compressed point lists; moves listed for any line of play, a board replayed with captures, ko and suicide rules and positional superko on request, and a position scored by area, with dead stones named by hand.4 views · 10 downloadsMazes maze.qwer · 1.0.0Mazes generated from a seed by eight algorithms, from a recursive backtracker's long corridors to Wilson's unbiased walks; solved by breadth-first search, measured for dead ends, loops and the longest path, loosened with loops by a braid setting, and drawn as text and as a compact SVG.3 views · 10 downloadsSudoku sudoku.qwer · 1.0.0Sudoku read from 81 characters or a drawn grid, validated with its conflicts named, solved with a count of solutions, generated from a seed with exactly one solution, graded by the techniques a person needs, from hidden singles to x-wings, and a next step given as an explained hint.5 views · 9 downloadsThread 0 posts
On Poker hands: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.