Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sml-poker

CI

A 5/6/7-card poker hand evaluator for a standard 52-card deck, in pure Standard ML — classify a hand into one of the nine standard categories (high card through straight flush, including the ace-low "wheel" A-2-3-4-5), and totally order any two hands to decide a winner, with kicker tie-breaks at every category.

No dependencies in the library itself: no FFI, threads, clock, or randomness. evaluate5, evaluateBest, and compare are pure functions of their card arguments and always agree between MLton and Poly/ML. Where a demo needs randomness (shuffling a deck), the seed is passed in explicitly — see examples/demo.sml, which vendors sml-prng as a demo-only dependency (also vendored, alongside sml-check, as a test-only dependency for the property-based checks).

  • evaluate5 scores exactly 5 cards.
  • evaluateBest scores the best 5-card hand out of 5, 6, or 7 cards — the natural shape for Texas Hold'em (2 hole cards + up to 5 board cards).
  • compare totally orders two handRank values: category first (a straight always beats three of a kind, regardless of numeric kickers), then a lexicographic comparison of tie-breaking kickers within a category.

API

signature POKER =
sig
  datatype suit = Clubs | Diamonds | Hearts | Spades
  type card = { rank : int, suit : suit }        (* rank 2..14, Ace = 14 *)

  datatype category =
      HighCard | OnePair | TwoPair | ThreeOfAKind | Straight
    | Flush | FullHouse | FourOfAKind | StraightFlush

  exception InvalidHand of string

  val mkCard : int * suit -> card
  val suitToChar   : suit -> char
  val suitOfChar   : char -> suit option
  val rankToString : int -> string                (* 2.."9","T","J","Q","K","A" *)
  val rankOfString : string -> int option
  val cardToString   : card -> string              (* e.g. "AS", "TD", "9H" *)
  val cardFromString : string -> card option
  val fullDeck : card list                         (* all 52 cards *)

  type handRank
  val categoryOf   : handRank -> category
  val categoryName : category -> string
  val kickersOf    : handRank -> int list

  val evaluate5    : card list -> handRank         (* exactly 5 cards *)
  val evaluateBest : card list -> handRank         (* best 5-of-{5,6,7} *)
  val compare : handRank * handRank -> order       (* GREATER = first hand wins *)
  val beats : handRank * handRank -> bool
  val ties  : handRank * handRank -> bool

  val combinations : int -> 'a list -> 'a list list
end

Example

val aces  = map (valOf o Poker.cardFromString) ["AH","AD","AC","KH","KD","2C","3D"]
val hr    = Poker.evaluateBest aces               (* best 5 of 7: aces full of kings *)
val "Full House" = Poker.categoryName (Poker.categoryOf hr)
val [14,13]       = Poker.kickersOf hr

val wheel  = Poker.evaluate5 (map (valOf o Poker.cardFromString) ["5C","4D","3H","2S","AC"])
val trips  = Poker.evaluate5 (map (valOf o Poker.cardFromString) ["8C","8D","8H","4S","2C"])
val true   = Poker.beats (wheel, trips)            (* any straight beats any trips *)

Running examples/demo.sml with make example prints:

sml-poker demo: 3-handed Texas Hold'em showdown
Deterministic seed 0wx5EED1234ABCD5678 (SplitMix64 shuffle)

Board: KD AS 3C 8H AC

Player 1 hole: AD 2S  ->  Three of a Kind [14,13,8]
Player 2 hole: 4S 9S  ->  Pair [14,13,9,8]
Player 3 hole: 5D JS  ->  Pair [14,13,11,8]

Player 1 wins with Three of a Kind [14,13,8]

A few fixed hands, scored directly with evaluate5:
  A-S K-S Q-S J-S T-S  ->  Straight Flush [14]
  7-C 7-D 7-H 7-S 2-C  ->  Four of a Kind [7,2]
  5-C 4-D 3-H 2-S A-C  ->  Straight [5] (the wheel: ace plays low)
  royal flush beats quads: true
  wheel beats quads: false

Build & test

Requires MLton and/or Poly/ML.

make test        # build + run the suite under MLton
make test-poly   # run the suite under Poly/ML
make all-tests   # both + byte-identical verification
make example     # build + run the demo (MLton)
make example-poly # run the demo (Poly/ML)
make clean

Installing with smlpkg

smlpkg add github.com/sjqtentacles/sml-poker
smlpkg sync

Reference lib/github.com/sjqtentacles/sml-poker/poker.mlb from your own .mlb (MLton / MLKit), or feed sources.mlb to tools/polybuild (Poly/ML).

Layout

sml.pkg                                     smlpkg manifest
Makefile                                    MLton + Poly/ML targets
.github/workflows/ci.yml                    CI: MLton + Poly/ML
lib/github.com/sjqtentacles/sml-poker/
  poker.sig      POKER signature
  poker.sml      evaluator implementation
  sources.mlb    ordered source list
  poker.mlb      public basis
lib/github.com/sjqtentacles/sml-prng/       vendored (test + demo only)
lib/github.com/sjqtentacles/sml-test/       vendored (test only, sml-check bridge)
lib/github.com/sjqtentacles/sml-check/      vendored (test only)
examples/
  demo.sml       seeded 3-handed Hold'em showdown + fixed-hand walkthrough
test/
  harness.sml    shared assertion harness
  test.sml       94 checks: examples, tie-breaks, exhaustive enumeration, properties
  entry.sml / main.sml
tools/polybuild   Poly/ML build wrapper

Tests

94 deterministic checks, run with make all-tests to verify identical output under both compilers:

  • Card/suit/rank helpers and fullDeck — string round-trips, invalid input, deck size and composition.
  • evaluate5 category classification — one hand-picked, hand-verified example per category (royal flush, 9-high straight flush, the steel-wheel straight flush, quads, full house, flush, straight, the rainbow wheel, trips, two pair, one pair, high card), plus InvalidHand on the wrong card count.
  • compare/beats/ties — category dominance (any straight beats any three of a kind even though the wheel's numeric kicker, 5, is smaller than the trips' kicker, 8 — a regression check against naive single-number encodings), kicker tie-breaks within a category (pair kickers, two-pair second pair, broadway vs. king-high straight vs. the wheel, flush second-card, quad kicker, full-house pair), and cross-suit ties.
  • evaluateBest on 6/7-card hands — Hold'em-style scenarios: a set becoming a full house, a straight beating a leftover pair, a 5-card flush out of 7 cards, and best-kicker selection out of 6 cards.
  • Exhaustive enumeration of all C(52,5) = 2,598,960 5-card hands — tallies every hand's category and asserts the counts match the well-documented standard poker combinatorics (see Wikipedia, "Poker probability"): 1,302,540 high card; 1,098,240 one pair; 123,552 two pair; 54,912 three of a kind; 10,200 straight; 5,108 flush; 3,744 full house; 624 four of a kind; 40 straight flush (4 of which are royal flushes) — a strong, self-checking oracle that a classifier bug is very unlikely to satisfy by accident. Runs in well under a second under either compiler.
  • Properties (sml-check)evaluateBest is invariant under reordering its input cards (reversal and rotation), and beats/ties agree exactly with compare.

License

MIT. See LICENSE.

About

7-card poker hand evaluator in pure Standard ML

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages