Games

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

#poker#cards#game#holdem#omaha

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 error

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.

poker_omaha(hole: list, board: list) → anyor an error

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.

poker_showdown(hands: any, board: list, options: any) → anyor an error

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" }.

poker_equity(hands: any, board: list, options: any) → anyor an error

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.

poker_deck(seed: int) → list

A 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 error

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.

poker_hand_class(hole: list) → stror an error

Two 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.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/poker.qwer$ curl -O https://quillolang.org/plugins/files/poker.qwe

Then, in your program, all of it or only what you call:

main.qwe
use poker.qweruse poker_evaluate, poker_omaha from poker      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

poker.qwe
// 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.qwer
//! 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' }) })}
Thread
39Useful? Like it, or add to its thread below.

Thread 0 posts

On Poker hands: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.