As the first guest blogger of Isaac’s I feel like I should say something about myself:
I am an in-progress Mathematician, Physicist and Computer Scientist, my present degree being a Joint MSci in Maths and Physics with Computer Science made to fit in as much as my University allows me to (1/3 in 1st year and slowly decreasing). I am also a huge Functional Programming geek, and have proudly been for 2 years now. Now, this blogpost is about Poker and Haskell.
Last year, as a new personal project, I decided that analysing the probabilities of a live Poker game was an interesting puzzle from both a mathematical and a programming point of view.
Yes, I do realise that the companies who actually need this information simply query a large database with all cards combinations, but my version of the program is far more complex and interesting than simply generating said database, and perhaps even unique because no one chooses it in virtue of its logical complexity. Side note: I did generate the database anyway in very few lines of code after setting up some Algebraic Data Types early on through development just to test it out, but it was too resource-expensive to be practical.
ADTs (with Type Classes) are FP Data Types with kind of greater or equal power as but infinitely simpler than Objects and Classes for you OO people; here is the simplest example from the program:
data Suit = Spades | Clubs | Diamonds | Hearts deriving (Eq, Ord, Enum, Bounded, Show, Read)
This single line makes Suit an ADT with 4 Value Constructors, which are Equatable, Orderable, Enumerable, Bounded, and can be Shown as and Read from strings; this means that all the obvious functions you might want to apply to them exist and work right out of the box (think of it as beefed-up class inheritance).
Calculating Poker probabilities abstractly is as easy as generating that database (it is even on Wikipedia!), but reworking them for the information which is slowly revealed in a game round and without actually calculating that precious database data is not. At all.
The current version of the program contains a good Data Types infrastructure, a basic shell to track the game, a set of functions to recognise Hand types, combinatorial counting functions, unique Hand ranking functions and various other stuff to tie it all together.
Very basically, whenever the player asks for the probability of something, this is what happens (only the steps needed by the specific request are executed):
- Everything is checked for changes since the last request and the following recalculated if there are any;
- The Table Cards are analysed and their Hand ranked absolutely (for the purposes of the other players’ chances), then the Player Cards are added and the same procedure is repeated;
- Then the number of better Hands which can exist at that stage in the game are calculated for both sets, which means cleverly subtracting amounts from the total number of better Hands;
- Then the specific request is answered: it could be what the best Hand the player can get at that turn is, or simply (not in the least) what the probability of winning that round is.
Obviously, all of this is heavily parallelised thanks to Haskell’s excellent libraries built on its intrinsic parallelizability (it literally takes 3 words to parallelise pretty much anything).
Look away now if you do not want to see some code I used to test my counting functions or learn a bit about Haskell:
-- Test whether any of the count functions of the given HandTypes yields a -- HandType which is not its own. In particular, lookout for ones higher -- than it. Also, count the instances of each checkHandTypes, checkHandTypesPar :: [HandType] -> [Card] -> [Card] -> [(HandType, [HandType])] checkHandTypes hts ocs cs = filter (not . null . snd) $ map (bad . getChecks) hts where bad (ht,chts) = (ht, nub . sort $ filter (/= ht) chts) getChecks ht = (ht, checkHtc ht ocs cs) -- Parallel Version checkHandTypesPar hts ocs cs = checkHandTypes hts ocs cs `using` parList rdeepseq
The first non-comment line is a Type Signature, and it helps the Programmer follow the program flow and the Compiler check for errors; you will find that in Haskell if something compiles, the program usually works perfectly on the first try (which is an amazing feeling).
The last 3 words of the last line intelligently parallelise the whole first function.
Sadly, sometime after the first kind-of-working version I took a break and lost interest because of a hard choice on the next step of its development between a purely mathematical rework and a purely formal rework in order to address the divergence of probabilities from the real ones in some situations (I knew something was up when I received a probability greater than 100%!).
I might pick it up again in the future, as it was immense fun at every stage.
I made the repository public yesterday for no reason at all, so Isaac asked me to write about it without delving too deep into the depths of Haskell.
If you are new to FP everything will look either incomprehensible or incredibly clear at first sight, and after some time just plain weird in both cases. The stand-out factor will definitely be the conciseness of it all (many crucial functions are one-liners).
Have a look if you like: https://github.com/Dr-Lord/Poker-Analyser.
If people are interested in either Poker or FP I could expand on either and/or become a recurring guest. If you are looking for a funnier article than this on FP, The Register has a great one.
Thanks to Thomas for writing this post about his Poker Analyser!
Enjoyed my post? Sign up to the newsletter to receive a small email when I post. No spam, I promise.