Last week I reluctantly shared the ugle initial C# version of my simple playing AI “nextCard”. Today, let’s look at the nicer version. One of the first things I did in F# was make getSuit and getRank helpers:
let getSuit (c: Desi.Card) : Desi.Suit = match c with | Desi.Card (s,r) -> s
let getRank (c: Desi.Card) : Desi.Rank = match c with | Desi.Card (s,r) -> r
I can now nicely sort my Rank:
let orderedCards = List.sortBy (fun x-> helper.getRank(x)) hand
That already looks nicer than its C# counterpart:
//order the cards from high to low var orderedHand = hand.OrderBy(x => x.Item2);
But, both these statements have the issue that they sort from low to high, thus, 2’s come first. That is not what we want. I considered changing the values of cards such that Ace has 1, but ultimately I decided against it, as in the card valuation, Ace must have the most points. Giving it value 14 means we can nicely deduct 10 from the ‘picture’ cards to obtain the value, which is surely better than 4 minus the card’s value. More philosophically speaking, we want the enum to align with the intuition of the user, aka, Ace is highest. But that means we have to reverse the order when sorting the cards:
let orderedCards = List.sortBy (fun x-> helper.getRank(x)) hand |> List.rev
With that, the rest of the transformation is quite straightforward, and the equivalent code in F# is something like this:
let nextCard {Direction = direction; Hand = h} (history : List<List>) (trump: Desi.Suit) : Desi.Card = match h with | Desi.Hand hand -> let orderedCards = List.sortBy (fun x-> helper.getRank(x)) hand |> List.rev let thisTrick = List.nth history 0 match thisTrick with | [] -> List.nth orderedCards 0 //nothing the in history yet, we are the starting player, and we play our highest card. | openCard :: t -> //first: can we follow suit? let openSuit = openCard |> helper.getSuit let allOpenSuit = List.filter (fun x -> helper.getSuit(x) = openSuit) thisTrick let openRank = openCard |> helper.getRank let allOpenSuitinOrder = List.sortBy (fun x-> helper.getRank(x)) allOpenSuit |> List.rev match allOpenSuitinOrder with | h::t -> if helper.getRank(h) > openRank then h else List.nth (List.sortBy (fun x-> helper.getRank(x)) allOpenSuit) 0 | [] -> //no can do? Let's check trumps! let allTrumps = hand |> List.filter (fun x-> helper.getSuit(x) = trump) let allTrumpsinOrder = List.sortBy (fun x-> helper.getRank(x)) allTrumps |> List.rev //if we have a trump, we will play it match allTrumpsinOrder with | h::t -> h | [] -> //otherwise we will play the highest card we have let orderedHand = List.sortBy (fun x-> helper.getRank(x)) hand |> List.rev List.nth orderedHand 0
I really like this code a lot more than the C# counterpart. Especially the sort of implicit null checks with patterns matching are easier to digest, don’t you think?
A few remarks and improvements:
- In this version I keep sorting the cards at every step, which is not needed, as, as far as I have been able to validate, filter respects the order of the list. However, 1) I am not sure this is always true (I checked the specs and I did not find it) and 2) it might make the code less readable if I remove them. In the end I opted to delete them but I am not 100% happy about it. I did leave the variable names, so allTrumpsinOrder even though I do not sort them, to make it a bit more clear.
- The part “List.nth (List.sortBy (fun x-> helper.getRank(x)) allOpenSuitinOrder) 0” is a bit cryptic. What I do is I sort the cards without the reverse, so they go low to high, and then I take the first one. Alternatively, I have been thinking about resorting orderedCards. That feels a bit silly (a