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).
evaluate5scores exactly 5 cards.evaluateBestscores 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).comparetotally orders twohandRankvalues: 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.
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
endval 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
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 cleansmlpkg add github.com/sjqtentacles/sml-poker
smlpkg syncReference lib/github.com/sjqtentacles/sml-poker/poker.mlb from your own
.mlb (MLton / MLKit), or feed sources.mlb to tools/polybuild (Poly/ML).
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
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. evaluate5category 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), plusInvalidHandon 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.evaluateBeston 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) —
evaluateBestis invariant under reordering its input cards (reversal and rotation), andbeats/tiesagree exactly withcompare.
MIT. See LICENSE.