Skip to content

tonyday567/poker-fold

Repository files navigation

poker-fold

A Haskell poker library experimenting with speed, static analysis & strategy.

Development

The library is in an active development phase, nothing is guaranteed to stay in place, and there is no centralised API.

One way to approach the library and appreciate the state of development is to have a good look around this org file. All the code is runnable in an emacs environment (and you can always cut and paste the code to elsewhere if this doesn’t suit).

There are also doctests everywhere so it’s relatively easy to find your way.

imports

:r
:set prompt " > "
:set -Wno-type-defaults
:set -Wno-name-shadowing
:set -XOverloadedStrings
import Poker.Card
import Poker.Card.Storable
import Poker.HandRank
import Poker.Range
import Poker.Random
import Poker.Lexico
import Poker.Charts
import Chart hiding (Range)
import Data.FormatN
import Optics.Core
import Prettyprinter
import qualified Data.Text as Text
import qualified Data.Map.Strict as Map
import qualified Data.Vector.Storable as S
import Data.Functor.Rep
putStrLn "ok"
Ok, 8 modules loaded.



















ok

quick tour

Cards

Poker.Cards contain the basic and human-friendly Poker types, and Poker.Cards.Storable the newtyped-wrapped, efficiently-stored versions. There are optics-core iso’s to help convert between the two versions, and prettyprinter is used to render it all nicely when you need to see results.

import Poker.Card
import Poker.Card.Storable
import Optics.Core
import Prettyprinter
cs' = [Card Ace Hearts,Card Seven Spades,Card Ten Hearts,Card Five Spades,Card Six Clubs, Card Seven Hearts,Card Six Spades]
cs = review cardsI cs'
cs
pretty cs

Routines for evaluation of a hand - 7 cards consisting of 2 player cards and 5 community cards - can be found in Poker.HandRank.

Hand Ranks

import Poker.HandRank
handRank cs
pretty (handRank cs)

It is much faster to look up hand rankings then recompute them every time, so a big Data.Vector.Storable is used to hold the indexed results.

To install the vector:

cabal install --force
poker-fold-writes --hvs7

… and to use it …

s <- hvs7
lookupHRUnsorted s cs
pretty $ lexiToHR (lookupHRUnsorted s cs)
pretty $ allHandRanks !! 4301

The hand rankings are held in reverse lexicographical position order using Knuth’s revolving door R algorithm which makes it just a little bit fancy. From Poker.Lexico:

cs & unwrapCardsS & sort & toLexiPosR 52 7 :: Int
pretty $ lexiToHR $ s S.! 32513187

I suspect that the algorithm is a hyperfunction

-- | Given a reverse lexicographic position, what was the combination?
--
-- >>> (\xs -> xs == fmap (fromLexiPosR 5 2 . toLexiPosR 5 2) xs) (combinations 2 [0..4])
-- True
--
fromLexiPosR :: Int -> Int -> Int -> [Int]
fromLexiPosR n k p = go (n - 1) k ((binom n k - 1) - p) []
  where
    go n' k' p' xs =
      bool
        ( bool
            (go (n' - 1) k' p' xs)
            (go (n' - 1) (k' - 1) (p' - binom n' k') (n' : xs))
            (p' >= binom n' k')
        )
        xs
        (length xs == k)

Ranges

A major thematic of the library is that poker strategy is well encapsulated by a 13 by 13 grid representation of a starting hand.

Poker.Range contains the core types for this encapsulation. Under the hood of Range is a numhask-array array which has a nice representable functor instance from adjunctions. Using tabulate can be confusing at first, but it tends to lead to quite simple code pipelines.

import Control.Category ((>>>))
import Prettyprinter.Render.Text (renderStrict)
pretty $ (tabulate (pretty >>> layoutCompact >>> renderStrict) :: Range Text)

Poker.Chart contains chart elements to help visualize Ranges.

import Poker.Range
import Poker.Charts
writeChartOptions "other/base.svg" baseChart

other/base.svg

The percentage chance of winning heads-up given each starting hand looks somewhat like this:

(Just m) <- readSomeRanges
let o2 = m Map.! "o2"
writeChartOptions "other/o2.svg" $ percentChart o2

other/o2.svg

Executables

There are two executables included in the library:

poker-fold-writes

poker-fold-writes executes the various canned data that help speed up computation.

cabal install
poker-fold-writes --hvs7

Creates a Storable vector (called hvs7) containing the hand ranking of every 7 card hand.

poker-fold-writes --sims 100000

Writes results of simulations for various ranges, accessed via readSomeRanges.

(Just m) <- readSomeRanges
let o2 = m Map.! "o2"
let o9 = m Map.! "o9"

The expected value change for each starting hand, from a heads-up, 2 player table to a full, 9 player table, expressed in big blinds.

pretty $ lpad 5 . fixed (Just 2) <$> ((\o o' -> (o' * 9 - o * 2)) <$> o2 <*> o9)

other/pixelo9.svg

poker-fold-writes --charts

Writes the example charts to file.

This is equivalent to running:

import Poker.Charts
writeAllCharts

poker-fold-speed

poker-fold-speed contains performance results and testing routines.

To hack this in emacs:

(setq haskell-process-args-cabal-repl '("poker-fold:exe:poker-fold-speed"))

hand evaluation speed

poker-fold-speed
label1              label2              results

:

handRank            time                1.44e4
handRank afap       time                1.33e4
handRank ffap       time                5.23e6
handRank f|         time                5.35e6
handRank |f         time                1.36e4
handRank |f|        time                6.46e6

shuffling

poker-fold-speed --shuffle -n 100000
label1              label2              results

rvi - list          time                2.40e-1
rvi - list f        time                1.87e2
rvi - single        time                5.43e1
rvi - single f      time                5.53e1
rvil - single       time                2.78e3
rviv - list         time                6.00e-4
rviv - list f       time                1.04e4
rviv - single       time                1.58e3
rviv - single f     time                2.75e3
poker-fold-speed --shuffle -n 10000
label1              label2              results

rvi - list          time                2.46e-1
rvi - list f        time                2.01e2
rvi - single        time                5.54e1
rvi - single f      time                5.41e1
rvil - single       time                3.03e3
rviv - list         time                6.00e-4
rviv - list f       time                1.36e4
rviv - single       time                2.95e3
rviv - single f     time                4.10e3

Creating a list of random variates stays lazy as perf is WHNF in the output. Forcing the list fixes this. For a single rvi, the output is computed, and force being added is probably creating an intermediary.

rvil is a list version of rviv.

poker-fold-speed --shuffle -n 10000 --allocation +RTS -T -RTS
label1              label2              results

rvi - list          allocation          0
rvi - list f        allocation          5.21e2
rvi - single        allocation          0
rvi - single f      allocation          0
rvil - single       allocation          3.39e3
rviv - list         allocation          0
rviv - list f       allocation          4.13e4
rviv - single       allocation          7.69e3
rviv - single f     allocation          7.68e3
poker-fold-speed --shuffle -n 100000 --allocation +RTS -T -RTS
label1              label2              results

rvi - list          allocation          0
rvi - list f        allocation          5.43e2
rvi - single        allocation          0
rvi - single f      allocation          1.88e1
rvil - single       allocation          3.49e3
rviv - list         allocation          0
rviv - list f       allocation          4.13e4
rviv - single       allocation          7.60e3
rviv - single f     allocation          7.60e3

Something allocated to the heap for rvi - single, forced, harming performance.

handRankS

import Data.Bifunctor
:t count
fmap (fmap (bimap getSum ((/10000.0) . fromIntegral))) $ execPerfT ((,) <$> count <*> time) $ handRankS_P 10000

:

count :: Measure IO (Sum Int)
fromList [("flushS",(10000,1709.5718)),("kindS",(9239,1160.7222)),("ranksSet",(9703,1324.894)),("straightS",(9703,421.3668))]

footnotes

origins

Poker AI is my Haskell origin story, and I can trace it back to pokerpirate and a series of posts, such as Exploiting the sit-and-go that came out, way back in the day, that were often referred to. Years later, I looked up Mike’s work and came across Fast Nearest Neighbour Queries in Haskell.

Mike’s paper and code kicked the arse out of anything else that people were using, back in the day. I like fast. I speedrun megabase Factorio for relaxation. For some people, min-maxing stuff is like knitting.

Down the rabbit hole, I spent some time in subhask, a still remarkable archeological site, full of buried treasure. Another great dig site is HLearn; my perf library is a direct descendent of History Monad I stumbled across. Mike was kind of winding things down by that stage, and it never made it to Hackage. I learnt about how you could just turn stuff off with execStateT, and there are guarantees of zero cost.

I think the library is the fastest open-source, 7-card Hold’em evaluator within a factor of 1.

Since many of you have a rough idea of the play, here’s the main hand value compute:

handRank :: Cards -> HandRank
handRank cs =
  fromMaybe
    (kind (toRanks cs))
    ( flush cs
        <|> straight (ranksSet cs)
    )

It’s slightly more efficient to check for flushes, drop down to a straight check transforming ranks to a set, and then dealing with kinded hands (the collective term for 4-of-a-kind, 2-pair and so on). This replaces about eleventy million lines of bit-shifting wizardry that you can explore via poker-eval, written almost a decade ago by Lemmih. The library compiles on ghc-9.2.3 without a single modification since publication. As does copumpkin’s vector-mmap, virtually unchanged since written in 2010.

Poker AI suffered a technology shock over the course of about a week, celebrated in The Great Poker Hand Evaluator Roundup — Coding the Wheel (It’s a highly entertaining read). It was realised that lookup tables beat raw computation and that was that for any need to improve evaluation.

One social outcome of this disruption was that future poker AI development went closed-shop, into poker trainers and HUDs, and this is why I add the open-source tag to my claim.

revolving door

-- | Given a reverse lexicographic position, what was the combination?
--
-- >>> (\xs -> xs == fmap (fromLexiPosR 5 2 . toLexiPosR 5 2) xs) (combinations 2 [0..4])
-- True
--
fromLexiPosR :: Int -> Int -> Int -> [Int]
fromLexiPosR n k p = go (n - 1) k ((binom n k - 1) - p) []
  where
    go n' k' p' xs =
      bool
        ( bool
            (go (n' - 1) k' p' xs)
            (go (n' - 1) (k' - 1) (p' - binom n' k') (n' : xs))
            (p' >= binom n' k')
        )
        xs
        (length xs == k)

equivalence classes

Hole cards start off as a set of 52 * 51 possibilities, but Suit information is neutral in hold’em, so equivalance classes of Hole hands narrow down to 169 possibilities.

hvs7

hvs7 is a vector of hand rankings for 7 card vectors in reverse lexicographic order.

s <- hvs7
l = S.length s
l

The first element of the vector corresponds to:

hand0 = fromLexiPosR 52 7 0 & fmap fromIntegral & S.fromList & CardsS
"hand:" <> pretty hand0
"hand rank index:" <> pretty (s S.! 0)
"hand rank:" <> pretty (lexiToHR $ s S.! 0)

And the last element corresponds to:

hand1 = fromLexiPosR 52 7 (l-1) & fmap fromIntegral & S.fromList & CardsS
"hand:" <> pretty hand1
"hand rank index:" <> pretty (s S.! (l-1))
"hand rank:" <> pretty (lexiToHR $ s S.! (l-1))

And the one hundred millionth

handb = fromLexiPosR 52 7 (100000000-1) & fmap fromIntegral & S.fromList & CardsS
"hand:" <> pretty handb
"hand rank index:" <> pretty (s S.! (100000000-1))
"hand rank:" <> pretty (lexiToHR $ s S.! (100000000-1))
pretty $ lexiToHR 3141

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published