A [mini] poker DSL in F#

 
25 september 2009

Domain Specific Languages are hot! Some developers think it’s the ultimate way of programming and it won’t be long until we don’t do anything but making and using these things. According to Neal Ford, DSL’s are just an abstraction mechanism, though it is about time to use it with something else then objects. Well, lets start implementing one using F# as ‘host’ language which offers us some nice datastructures to get the job done. We’ll make a [mini] ‘Poker’ DSL which due to its succinctness fits easily into a blog using F#.

Poker cards

First of all I’d like to discuss some datastructures. The ones we use are discriminated unions and (.NET) enums. Starting to sound like Chinese? OK, let’s have a closer look at it. A discriminated union is a recursive datastructure representing one out of a finite set of choices. A (.NET) enum is much alike, however it uses an integer as underlying representation. These examples show you that it isn’t as hard as you might think.

type card = |Ace = 14 |Two = 2 |Three = 3 |Four = 4 |Five = 5 |Six = 6
            |Seven = 7 |Eight = 8 |Nine = 9 |Ten = 10 |Jack = 11 |Queen = 12 |King = 13

This .NET enum tells us that a typical card could be an Ace, Jack, Queen and so on. Why using it? Well can you suggest a better way to tell the compiler a Queen has a higher value then a Jack?

Thanks to this our compiler knows how to play with cards! Let’s do a little excersise:

card.Ace < card.Two

Returns:

val it : bool = false

F# behaves like a good student, but passing just one card at a time doesn’t simulate a real poker game. In in common poker we use ‘hands’ with five cards at a time instead. So let’s turn our attention to the discriminated union.

type Hand = | Cards of card * card * card * card * card

We want F# to use the term ‘hand’ as a synonym for  ‘five cards’. Now we can use ‘hands’ instead of cards which again, is a piece of cake for F#.

let hand = Cards(card.Ace, card.Seven, card.Three, card.Four, card.Jack)

let straightHand = Cards(card.Two, card.Three, card.Four, card.Five, card.Six)

hand > straightHand

val it : bool = true

Nice! F# can handle ‘hands’ now, using the underlying enum intergers to make a comparison, but there’s still a problem. Our straight ‘hand’ does indeed have cards of less value, but the ‘straight’-combination gives a higher result than the cards seperately. At the moment this occurance is meaningless to F#. We need a way to recognize the combination.

To solve this, we just add those combinations that are higher valued, to the discriminated union and write some transformations for them to parse hands.

First step: add combinations like ‘full-house’ and ‘straight’

type Hand =
 | Cards           of card * card * card * card * card
 | Full_house      of int // 300
 | Straight        of int // 200
 | Three_of_a_kind of int // 100

We fool around a bit adding some integers for each combination but we will fix this shortly. Now F# ‘knows’ how to rate a straight mutch better:

let straightHand = Straight(200)

hand1 > straightHand

val it : bool = false

F# can handle Straight(200) because we added it to the Hand definition. Maybe this doesn’t impress you with the pragmatic integer assigning. Well, we just have stuff that under the hood and no one will ever know!

Second step: defining a function called ‘parseCombinations (hand:Hand)‘ which is defined below, alows us to parse a hand to a combination. For example combinations as full-house and straight. Now we have a nice PokerDSL. Let’s play a final game:

let hand = Cards(card.Ace, card.Seven, card.Three, card.Four, card.Jack)
let fullHouseHand = Cards(card.Two, card.Two, card.Two, card.Five, card.Five)
let straightHand = Cards(card.Two, card.Three, card.Four, card.Five, card.Six)
let straightHand2 = Cards(card.Nine, card.Ten, card.Jack, card.Queen, card.King)

getWinningHand hand fullHouseHand
val it : Hand = Cards (Two,Two,Two,Five,Five) // Full-House

getWinningHand fullHouseHand straightHand
val it : Hand = Cards (Two,Two,Two,Five,Five) // Full-House

getWinningHand straightHand straightHand2
val it : Hand = Cards (Nine,Ten,Jack,Queen,King) // Straight 2

Source:

type card = |Ace = 14 |Two = 2 |Three = 3 |Four = 4 |Five = 5 |Six = 6
            |Seven = 7 |Eight = 8 |Nine = 9 |Ten = 10 |Jack = 11 |Queen = 12 |King = 13

type Hand =
 | Cards           of card * card * card * card * card
 | Full_house      of int // 300
 | Straight        of int // 200
 | Three_of_a_kind of int // 100

let getValue (hand:Hand) =
 match hand with
  | Cards(a, b, c, d, e) -> (int a) + (int b) + (int c) + (int d) + (int e)
  | Full_house(a)        -> a
  | Straight(a)          -> a
  | Three_of_a_kind(a)   -> a

let distinct (l : List<int>) =
 let rec loop l (result : List<int>) =
  match l with
  | hd :: tl when (List.exists (fun x -> x = hd) result) -> loop tl result
  | hd :: tl -> loop tl (hd :: result)
  |[]        -> result
 loop l []

let distinctLen (l : List<int>) =
 List.length (distinct l)

let maxEqual (l : List<int>) =
 let rec loop l max =
  let l' = List.filter (fun x -> x <> (List.hd l)) l
  let dif = List.length l – List.length l'
  match l' with
  | []                 -> if max = 0 then 5 else max
  | _ when (dif > max) -> loop l' dif
  | _                  -> loop l' max
 loop l 0

let parseCombinations (hand:Hand) =
 match hand with
 | Cards(a, b, c, d, e) when maxEqual [(int a);(int b);(int c);(int d);(int e)] = 3
                             && distinctLen [(int a);(int b);(int c);(int d);(int e)] = 2
     -> Full_house(getValue hand + 300)
 | Cards(a, b, c, d, e) when maxEqual [(int a);(int b);(int c);(int d);(int e)] = 3
     -> Three_of_a_kind(getValue hand + 100)
 | Cards(a, b, c, d, e) when (e – d) = (d – c) && (c – b) = (b – a) && (int (b – a)) = 1
     -> Straight(getValue hand + 200)
 | _ -> hand // Full_house and Three_of_a_kind, etc.

let getWinningHand (hand1:Hand) (hand2:Hand) =
 if getValue (parseCombinations hand1) > getValue (parseCombinations hand2) then hand1 else hand2


Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Development

Tags: ,


Reacties (4)

  • Christiaan schreef:

    I don’t know F# but wouldn’t this also get parsed as a Full House in this example?

    Cards(card.Two, card.Two, card.Two, card.Two, card.Five)

    Geplaatst op 28 september 2009 om 20:39 Permalink

    • Andries Nieuwenhuize schreef:

      Thanks! Ik heb de code aangepast.

      Geplaatst op 29 september 2009 om 10:06 Permalink

      • Andries Nieuwenhuize schreef:

        Shame on me! Ik heb er nog meer foutjes uitgehaald.

        Geplaatst op 30 september 2009 om 9:22 Permalink

      • Christiaan schreef:

        Hmm, maar nu ziet hij dit waarschijnlijk als full house?

        Cards(card.Two, card.Two, card.Three, card.Five, card.Five)

        Geplaatst op 29 september 2009 om 19:10 Permalink