Example Sample Logbook 2
Example Sample Logbook 2
Andrei-Alin Murjan
May 31, 2021
1
Contents
1 Introduction 4
4 Implementation 27
4.1 The back-end . . . . . . . . . . . . . . . . . . . . . . . 27
4.1.1 Data modelling - The card . . . . . . . . . . . . 27
4.1.2 Data modelling - The hand ranking . . . . . . . 28
4.1.3 Data modelling - The challenge . . . . . . . . . 28
4.1.4 Data modelling - The player . . . . . . . . . . . 28
4.1.5 API - Cardistry . . . . . . . . . . . . . . . . . . 30
4.1.6 API - Engine - Utils . . . . . . . . . . . . . . . 33
4.1.7 API - Engine - Dealing . . . . . . . . . . . . . . 33
4.1.8 API - Engine - Actions . . . . . . . . . . . . . . 37
4.1.9 API - Engine - Hand evaluation . . . . . . . . . 44
4.1.10 API - Engine - Rank evaluation . . . . . . . . . 51
4.1.11 API - Controller . . . . . . . . . . . . . . . . . 55
4.2 The front-end . . . . . . . . . . . . . . . . . . . . . . . 69
4.2.1 Setup - Global state management . . . . . . . . 69
4.2.2 Setup - Interfaces . . . . . . . . . . . . . . . . . 76
4.2.3 Setup - Services . . . . . . . . . . . . . . . . . . 77
2
4.2.4 Setup - Components . . . . . . . . . . . . . . . 81
5 Testing 102
6 Conclusion 111
3
1 Introduction
This logbook documents the software development life cycle of cre-
ating a Five-Card Draw Poker game engine by using the declarative
programming paradigm and putting it in the context of a software ap-
plication that users can interact with through a web-based interface.
I chose to develop the Poker game engine because this project idea
has been on my mind for around a year. Due to not having enough
time to develop it in my spare time, it has been catching dust along
some other project ideas I had.
When I saw that a Poker game was one of the options we could pick
for the coursework, I was quite excited, as I had the opportunity to
start working on it.
4
The reason for sticking with React and TypeScript, instead of going
with Fable or Feliz and writing React code in F#, is simply that I
wanted to get a user interface up and running as fast as possible, since
my main focus for this project was the back-end. The front-end is sim-
ply a means of presenting the poker game engine that would otherwise
be a bit boring under just a CLI program.
5
3 Planning and Design
3.1 Target audience
This software project aims to be a simple Five-Card Draw Poker game
primarely made for casual players of Poker that look for an escape to
their boredom or for a ”background game” that they can play relatively
mindlessly while idling.
6
The Aces, Kings, Queens and Jacks can be represented in the fol-
lowing way:
7
3. The total number of chips they can bet. A game of Poker involves
betting chips. If the player is out of chips, they are out of the
game.
4. The challenge the player chose. The challenges are Bet, Call,
Fold, Raise or All In.
5. The amount bet by the player. When betting, each player chooses
their amount. It is important to keep track of that amount, as
all player challenges depend on it.
For example, if player A bets £150, player B can choose to call
and bet £150, raise that amount to something greater than £150,
fold due to intimidation or go all in if they have a good hand or
feel like their opponent is bluffing.
Although those 5 elements will suffice for a Poker game engine, in order
to create a better user experience, it is a good idea to have elements
such as:
4. Their hand’s highest rank, that can be used to let the user know
how strong their hand is.
8
P layerChallenge : Bet ∪ Call ∪ Raise ∪ F old ∪ AllIn
P layer ={
N ame : seq Char,
Hand : seq Card,
Score ∈ N0 ,
Chips ∈ N0 ,
BetAmount ∈ N0 ,
W onM atches ∈ N0 ,
LostM atches ∈ N0 ,
HighestRank : HandRanking ∪ ∅
(A hand can be empty, therefore the highest rank can be null)
check subsection 3.7 for definition
Challenge : P layerChallenge ∪ ∅
(The player’s challenge can be null if the game has started,
but no challenges have been made yet)
}
P = c ∧ s =⇒ (d =⇒ l)
9
Scenario : Dealing cards to p l a y e r s
Given The deck o f c a r d s i s c r e a t e d
And The deck o f c a r d s i s s h u f f l e d
When The c a r d s a r e d e a l t
P = h =⇒ (c =⇒ d ∧ x)
S c e n a r i o : D i s c a r d i n g a number o f c a r d s
Given The p l a y e r has a hand o f 5 c a r d s
When The p l a y e r c h o s e t o d i s c a r d a
number o f c a r d s g r e a t e r than 0
10
And The same number o f c a r d s i s
d e a l t t o t he p l a y e r
11
3.7.1 Hand ranking and new card data models
A hand ranking can be represented as follows:
Let HC := HighCard
Let P := P air
Let T P := T woP air
Let T K := T hreeOf AKind
Let S := Straight
Let F := F lush
Let F H := F ullHouse
Let F K := F ourOf AKind
Let SF := StraightF lush
Let RF := RoyalF lush
HandRanking = {HC ∪ P ∪ T P ∪ T K ∪ S ∪ F ∪ F H ∪ F K ∪ SF ∪ RF }
1. High card.
A high card ranking consists of having 5 cards with different
12
ranks and a variety of suits.
This ranking can be represented as follows:
2. Pair.
A pair ranking consists of having a hand with 2 cards whose
values are identical.
In essence, we need to scan the hand twice, checking each card
against each other to see if they are pairs. Of course, we don’t
count the same card twice.
This ranking can be represented through the following equation:
∀h : seqCard, ∀c : Card
V (c) = The value / rank of the card c (e.g. 10, or 14 for king)
ψ = Function that checks for a pair for one iteration
Ψ = Function that checks a pair for the subsequent iteration
true, V (c) = V (hi )
f alse, n = length(h)
ψi (h, c) =
ψi+1 (h, hi ), c = hi
ψi+1 (h, c), ∀h : seqCard, ∀c : Card
(true, V (c)), ψi (h, c) = true
Ψi+1 (h, hi+1 ), length(h) 6= 0
Ψi (h, c) =
ψi (h, c) = f alse ∧ i = (length(h) − 1)
(f alse, N one),
∀h : seqCard, ∀c : Card
P air(h) = Ψ0 (h, h0 )
13
Scenario : Pair
Given The p l a y e r has a hand o f 5 c a r d s
with 2 c a r d s whose v a l u e s a r e i d e n t i c a l
3. Two pair.
A two pair ranking consists of having a hand with 2 pairs.
In order to determine whether we have a two pair or not, we first
need to check whether the hand has a pair. If it does, then we can
remove it from the hand and check for a pair in the remainder of
the hand.
If 2 pairs have been found, then we have a two pair ranking.
This ranking can be represented through the following equation:
∀h : seqCard, ∀v : Rank
RP V (h, v) = Function that removes a pair from the hand
hp(h) = P air(h)0 , where hp = Has Pair
pv(h) = P air(h)1 , where pv = Pair Value
spv(h) = pv(RP V (h, pv(h))), where spv = Second Pair Value
length(RP V (h, pv(h))) = 0
(f alse, N one), hp(h) = f alse
T woP air(h) = hp(RP V (h, pv(h))) = f alse
(true, (pv(h), spv(h))), hp(RP V (h, pv(h))) = true
S c e n a r i o : Two P a i r
Given The p l a y e r has a hand o f
5 c a r d s with 2 p a i r s
4. Three of a kind.
A three of a kind ranking consists of having a hand with 3 cards
14
whose values are identical.
We can make use of the pair-related functions we have previously
defined in order to determine if we have a three of a kind. If we
have a pair in the hand, we need to find the next card whose
rank matches the pair’s rank. We can do this by removing the
cards that have the same rank as the pair. If we end up with 2
cards, then we have a three of a kind.
This ranking can be represented through the following equation:
∀h : seqCard, ∀v : Rank
V (c) = The value / rank of the card c (e.g. 10, or 14 for king)
RP V (h, v) = Function that removes a pair from the hand
hp(h) = P air(h)0 , where hp = Has Pair
pv(h) = P air(h)1 , where pv = Pair Value
x[i:] = Slice of the sequence ’x’, starting from index ’i’
(f alse, N one), hp(h) = f alse
Ω(f, h) =
f (pv(h), h), hp(h) = true
where ’f’ is a function that executes a check if a pair is found
length(h) = 0
(f alse, N one),
length(RP V (h[1:] , V (h1 ))) = 0
θ(v, h) =
(true, v), length(h) = 2orV (h0 ) = v
θ(V (h0 ), h), length(RP V (h[1:] , V (h1 ))) 6= 0
(f alse, N one), length(h) = 0
Θ(v, h) =
θ(v, h), length(h) 6= 0
T hreeOf AKind(h) = Ω(Θ, h)
S c e n a r i o : Three o f a Kind
Given The p l a y e r has a hand o f 5 c a r d s with
3 c a r d s whose v a l u e s a r e i d e n t i c a l
15
5. Straight.
A straight ranking consists of having a hand of 5 cards whose
values are consecutive
In order to determine whether we have a straight or not, we can
make use of the primes correspondent to the card’s rank. We
can compute a set of consecutive products of primes and check
whether our hand’s product of primes is a member of that pre-
computed set.
There is also the case where a player may have the following
straight: {Ace, 2, 3, 4, 5}, whose product of primes is 8610 (2 * 3
* 5 * 7 * 41)
∀h : seqCard
V (c) = The value / rank of the card c (e.g. 10, or 14 for king)
Vn (h) = seq{V (h0] ), V (h1 ), . . . , V (hn )}
φ(s, p) = Function that filters a sequence ’s’, based on the predicate ’p’
∀s ∈ N,
σ(sn ) = seq{s0 , σ(φ(s, (n) → n mod s0 6= 0))},
where σ is the Sieve of Eratosthenes
p = σ(seq{2, 3, 4, . . . , 41})
4
Q Q5 Q12
P = { pi , pi , . . . , p i }
i=0 i=1 i=8
l = length(h)
Q Q
(f alse, N one), Q Vl (h) 6= 8610 ∧ Q Vl (h) ∈
/P
Straight(h) =
(true, h), Vl (h) = 8610 ∨ Vl (h) ∈ P
Scenario : Straight
Given The p l a y e r has a hand o f 5 c a r d s
whose v a l u e s a r e c o n s e c u t i v e
16
Then The p l a y e r ' s rank i s S t r a i g h t
6. Flush.
A flush ranking consists of having a hand of 5 cards whose suits
are identical, regardless of value.
A way to determine whether we have a flush or not is by taking
the first card’s suit and comparing it to the rest’s suits. If even
one of them differs, then we don’t have a flush. Otherwise, we do.
∀h : seqCard, ∀s : Suit
S(c) = The suit of the card c (e.g. Hearts
p = h0
x[i:] = Slice of the sequence ’x’, starting from index ’i’
(true, s(p)), i = length(h)
Φi (h) = Φi+1 (h[1:] ), S(h0 ) = S(p)
(f alse, N one), S(h0 ) 6= S(p)
F lush(h) = Φ1 (h[1:] )
S c e n a r i o : Flush
Given The p l a y e r has a hand o f 5 c a r d s whose
s u i t s are i d e n t i c a l , r e g a r d l e s s of value
7. Full house.
A full house ranking involved having a hand whose ranking is a
pair and a three of a kind simultaneously.
17
∀h : seqCard, ∀v : Rank
V (c) = The value / rank of the card c (e.g. 10, or 14 for king)
RP V (h, v) = Function that removes a pair from the hand
hp(h) = P air(h)0 , where hp = Has Pair
ht(h) = T hreeOf AKind(h)0 , where ht = Has Triple
pv(h) = P air(h)1 , where pv = Pair Value
tv(h) = T hreeOf AKind(h)1 , where tv = Triple Value
length(h) = 0
(f alse, N one),
ht(h) = f alse
F ullHouse(v, h) =
(true, (pv(h), tv(h))), hp(h) = true
S c e n a r i o : F u l l House
Given The p l a y e r has a hand o f 5 c a r d s
with a p a i r and a t h r e e o f a kind
8. Four of a kind.
A four of a kind ranking consists of having a hand with 4 cards
whose values are identical.
18
∀h : seqCard, ∀v : Rank
V (c) = The value / rank of the card c (e.g. 10, or 14 for king)
RP V (h, v) = Function that removes a pair from the hand
hp(h) = P air(h)0 , where hp = Has Pair
pv(h) = P air(h)1 , where pv = Pair Value
x[i:] = Slice of the sequence ’x’, starting from index ’i’
(f alse, N one), hp(h) = f alse
Ω(f, h) =
f (pv(h), h), hp(h) = true
where ’f’ is a function that executes a check if a pair is found
(true, v), length(RP V (h)) = 1
Γ(V (RP V (h)0 ), RP V (h)[1:] ), length(RP V (h)) = 4
Γ(v, h) =
length(RP V (h)) = 0
(f alse, N one),
∀h : seqCard
F ourOf AKind(h) = Ω(Γ, h)
S c e n a r i o : Four o f a Kind
Given The p l a y e r has a hand o f 5 c a r d s
with 4 c a r d s whose v a l u e s a r e i d e n t i c a l
9. Straight flush.
A straight flush ranking consists of having a hand of 5 cards
whose values are consecutive and have the same suit. The high-
est card rank must not be an Ace.
19
∀h : seqCard, ∀v : Rank
is(h) = Straight(h)0 , where is = IsStraight
if (h) = F lush(h)0 , where if = IsFlush
true, is(h) ∧ if (h)
StraightF lush(h) =
f alse, (¬is(h)) ∨ (¬if (h))
S c e n a r i o : S t r a i g h t Flush
Given The p l a y e r has a hand o f 5 c a r d s
without an Ace , whose v a l u e s a r e
c o n s e c u t i v e and have t h e same s u i t
∀h : seqCard, ∀v : Rank
V (c) = The value / rank of the card c (e.g. 10, or 14 for king)
if (h) = F lush(h)0 , where if = IsFlush
Q
true, if (h) ∧ QVlength(h) (h) = 31367009
RoyalF lush(h) =
f alse, (¬if (h)) Vlength(h) (h) 6= 31367009
S c e n a r i o : Royal Flush
Given The p l a y e r has a hand o f 5 c a r d s
with an Ace , whose v a l u e s a r e
c o n s e c u t i v e and have t h e same s u i t
20
i n t h e i r hand
21
∀h : seqCards, c : Card
P (c) = The prime value of the card c (e.g. 41 for Ace)
Y
h = The hand’s product of primes
A = P (Ace )
K = P (King )
Q = P (Queen )
J = P (Jack )
T = P (10 )
N = P (9 )
Y
HighCardScore = h
P airScore = HighCardScore + A
T woP airScore = P airScore + A2
T hreeOf AKindScore = T woP airScore + K 2
StraightScore = T hreeOf AKindScore + A3
F lushScore = StraightScore + A, K ∗ Q ∗ J ∗ T
F ullHouseScore = F lushScore + A ∗ K ∗ Q ∗ J ∗ N
F ourOf AKindScore = F ullHouseScore + A3 ∗ K 2
StraightF lushScore = F ourOf AKindScore + A4
RoyalF lushScore = StraightF lushScore + K ∗ Q ∗ J ∗ T ∗ N
As you can see from the algorithm described above, the score of the
higher ranking is always dependent on the previous rank score, plus
what the strongest hand for the previous rank is.
To exemplify: the score of any pair should be greater than the highest
possible card rank, that being the Ace. Thus, in order to calculate the
pair’s score, we add the score of an Ace to the current prime product.
22
Analogously, we do the same for the rest of the ranks.
Here is another example describing the royal flush: the score of a royal
flush should be greater than the highest possible straight flush, that
being a King-High straight (KQJTN). Thus, in order to calculate the
royal flush’s score, we add the score of the King-high straight hand to
the straight flush rank score.
23
s h o u l d be 50 USD
And The p l a y e r s h o u l d have 50 USD
l e s s then they had b e f o r e b e t t i n g
And The pot s h o u l d t o t a l t o 50 USD
24
When The p l a y e r ' s c h a l l e n g e i s a R a i s e
And The p l a y e r ' s amount i s g r e a t e r th e
opponent ' s amount by $ 1
4. Going all in: To go all in means for a player to bet all the chips
they currently own. After a player goes all in, the opponent can
only challenge them by folding or calling.
S c e n a r i o : Going A l l In
25
When The p l a y e r c a l l s t h e p r e v i o u s p l a y e r ' s amount
26
4 Implementation
Since this is a web app, it will require a standard web architecture -
a back-end, where the business logic will live, and a front-end, where
the UI and the game flow will live.
// Card Rankings
type Rank =
| Ace of int
| Jack of int
| Queen of int
| King of int
| Common of int // Anything other than the above are default
numbers.
// I'll annotate them as "Common"
27
}
type Player = {
Name: string
28
mutable Hand: Card list
mutable Score: int
mutable Chips: int
mutable Bet: int
mutable WonMatches: int
mutable LostMatches: int
mutable HighestRank: HandRanking option
mutable Challenge: PlayerChallenge option
}
The reason for making almost all of the record type fields mutable is
that I wanted the player operations to be done in-place.
Then again, having mutable fields leads to the creation of impure func-
tions, which beats the point of the declarative paradigm.
29
4.1.5 API - Cardistry
Before defining card and deck operations, I had to create some utility
functions that will help in shuffling the deck and doing prime number
operations.
module Utils =
let swap x y (arr: Model.Card[])=
let tmp = arr.[x]
arr.[x] <- arr.[y]
arr.[y] <- tmp
let rec sieve lst =
match lst with
| h::t -> h :: (sieve <| List.filter
(fun num -> num % h <> 0) t)
| [] -> []
The sieve function takes a list of integers and returns a new list con-
structed by filtering out all elements that are divisors of the head of
the list.
With the help of those functions, I was able to define some card oper-
ations that are responsible with deck building, as well as formatting.
Below you’ll find the code I wrote to create an ordered and unshuffled
deck:
// Here the suits and ranks are defined
let suits = [Model.Hearts; Model.Clubs; Model.Spades;
Model.Diamonds]
let rankVals = [
Model.Common 2; Model.Common 3; Model.Common 4;
Model.Common 5; Model.Common 6; Model.Common 7;
Model.Common 8; Model.Common 9; Model.Common 10;
30
Model.Jack 12; Model.Queen 13; Model.King 14; Model.Ace 15;
]
let primes = Utils.sieve [2 .. 41]
let ranks =
rankVals
|> List.mapi (fun i rankVal -> (rankVal, primes.[i]))
// This is where we generate the initial and unshuffled deck
let initialDeck = [|
for suit in suits do
for rank in ranks do
yield {
Model.Suit = suit;
Model.Rank = rank
}
|]
initialDeck
|> Array.iteri (fun i _ ->
let randIndex = rand.Next(i, Array.length initialDeck)
Utils.swap i randIndex initialDeck)
List.ofArray initialDeck
The following are some formatting functions that I made use of later
in development
31
let getValue (card: Model.Card) =
match card.Rank with
| (Model.Ace v, p) -> (v, p)
| (Model.Jack v, p) -> (v, p)
| (Model.Queen v, p) -> (v, p)
| (Model.King v, p) -> (v, p)
| (Model.Common v, p) -> (v, p)
32
4.1.6 API - Engine - Utils
The first utility function involves removing a given card from a list
of cards, while the second involves removing the first card in a list of
cards.
33
the top of the deck, giving it to the player and removing it from the
deck.
The following 2 functions are responsible with the main card dealing
mechanic. The first function’s purpose is to deal cards to a particu-
lar player, while the second function’s purpose is to deal cards to all
players in the game.
// In five-card poker, cards are dealt in 5s
let rec _dealCards
(steps: int)
(numSteps: int)
(player: Player)
(deck: Card list) =
34
Aside from card dealing, there is also discarding.
In Five-Card Draw, the first phase of the game (after the cards have
been dealt) is the discarding phase, where players can choose to dis-
card all, some or none of the cards in their hands.
35
let discardCards
(player: Player)
(options: int list)
(isCPU: bool)
(deck: Card list) =
let discardChoice() =
let choices =
if isCPU = false then
match options.Length with
| 0 -> [0]
| _ -> options |> List.distinct
else
[0 .. 6]
|> Set.ofList
|> Set.intersect (set [ for _ in [0 .. 6] do
rand.Next 7 ])
|> Set.toList
The first function is responsible with discarding cards from the hand
and dealing others in place, given a list of choices.
The list of choices represents numbers from 0 to 6, where 0 means
”don’t discard anything” and 6 means ”discard all cards”. Any choice
between 0 and 6 represents the index of the card to be discarded plus
1. In hindsight, perhaps it would’ve been a good idea to map those
choices into a discriminated union type, as it would’ve been a bit easier
to read and follow. Yet, since this isn’t that complex of a function, I
think using a list of integers is good enough.
36
The second function is responsible with creating / sanitizing the list of
choices and dispatching discard actions based on the player kind (user
or CPU). If the player is the CPU, then we randomly select a unique
list of choices. Otherwise, we use the choices given by the player.
The following functions are used later in this module to carry out
the different challenge actions:
let private getPlayerBet (bet: int) (totalChips: int)
(minimumAmount: int) =
let minimumAmount =
if totalChips < minimumAmount then totalChips
else minimumAmount
let invalidBetConditions =
(bet <= 0),
(bet < minimumAmount && totalChips >= minimumAmount),
(bet > totalChips)
37
let private setBet (player: Player) (amount: int) =
player.Bet <- amount
player.Chips <- player.Chips - amount
38
The following is the function responsible with dispatching the bet
action:
let private bet
(player: Player)
(amount: int)
(_pot: int)
(isUser: bool) =
Aside from the error handling, this function simply returns the current
player and the pot computed by calling the getPotAfterBet function.
In order to bet, the bet amount must be at least $50. This ensures
that there are no low stakes (e.g. a $1 bet).
Having really low stakes can make a poker game, or any game that
involves betting, extremely unrewarding due to the low risk - low re-
ward characteristic of such a bet.
39
The following function implements the folding mechanic, which is
nothing other than setting the player’s bet amount to 0 and their
challenge to ”Fold”.
let private fold (player: Player) (pot: int) =
setBet player 0
player.Challenge <- Some Fold
player, pot
40
The following 2 functions implement calling and going all in:
let private call (player: Player) (previousBet: int) (pot: int) =
if player.Chips < previousBet then
setBet player player.Chips
else
setBet player previousBet
player.Challenge <- Some Call
player, pot + previousBet
41
After defining all the actions linked to the challenges, I defined a
function that dispatches an action given a challenge.
let getChallenge
(isUser: bool)
(challenge: PlayerChallenge option)
(amount: int)
(previousBet: int)
(pot: int)
(currentChallenge: PlayerChallenge option)
(player: Player) =
let opt =
match isUser with
| true -> challenge
| false ->
let choices = [Some Bet; Some Fold; Some AllIn]
if ((rand.Next 100) + 1) % 10 = 0 then Some AllIn
else
match currentChallenge.IsNone with
| true -> choices |> List.item (rand.Next
(choices.Length))
| false ->
let choices = Some Call :: Some Raise ::
choices.Tail
if ((rand.Next 100) + 1) % 15 = 0 then Some
Raise
else choices |> List.item (rand.Next
(choices.Length))
42
If the player is the CPU, we generate the challenges randomly. For
a better experience, there is a 10% change of the CPU going all in and
a 15% chance of it raising.
I also defined some functions that make the calls of getChallenge more
convenient though partial application.
let getPlayerChallenge = getChallenge true
let getCPUChallengeExplicit = getChallenge false
let getCPUChallenge = getChallenge false None 0
let CPURespondToAllIn
(cpu: Player)
(previousBet: int)
(pot: int) =
43
4.1.9 API - Engine - Hand evaluation
/// Pair
// Function used to iterate once through the hand and check
if the card that is
// currently being used has the same rank value as a card
iterated through the hand
let rec private _checkPairForIter (hand: Card list) (card:
Card) (iteration: int) =
// If we finished iterating, it means we've found nothing,
// so we'll return false
if iteration = hand.Length then false
else
// Match the hand of cards with the following patterns:
match hand with
// If the card we are currently checking is the same
as the item
// at the current iteration, keep using the card from
the current iteration and
// go to the next iteration.
| h when (isFromSameIndex h card iteration) ->
_checkPairForIter
h
(h |> List.item iteration)
(iteration + 1)
44
// If the value of the item at the current iteration
is the same as
// the card we are currently checking, then we found a
pair
| h when getValue (h |> List.item iteration) =
getValue card -> true
45
| _ -> (false, None)
// Two Pair
let isTwoPair (hand: Card list) =
// Create a shallow copy of the hand
let handCopy = hand
46
match handCopy with
| [] -> (false, None)
| _ ->
// Check if we have a pair once again
let (hasPair, secondValue) = isPair handCopy
// Three of a Kind
let _isThreeOfAKind (prevPairValue: Rank) (hand: Card list) =
let handCopy = removePairValue hand prevPairValue
// Straight
// A straight can be computed by looking up the product of
47
the hand
// into a list of prime products.
// E.g. A 6 High-Straight would have a product of 2310, which
is
// the following prime product: 2 * 3 * 5 * 7 * 11
let isStraight (hand: Card list) =
let primes = sieve [2 .. 41]
let handPrimeProduct =
hand
|> List.map (fun card -> (card.Rank |> snd))
|> List.reduce (fun product prime -> product * prime)
// Flush
let isFlush (hand: Card list) =
// We get the first card from the hand
let prev = hand.Head
// Otherwise
48
else
// Match the tail of our hand with the following
patterns
match _hand with
// Let curr be the head of the list
// If the suit of the head card is equal to the
// suit of the head card, we iterate through the
hand again
| curr::restOfHand when curr.Suit = prev.Suit ->
_isFlush restOfHand (iteration + 1)
// Full house
let isFullHouse (hand: Card list) =
let handCopy = hand
if hasTriple then
let handCopy = removePairValue handCopy
tripleValue.Value
// Four of a kind
let rec _isFourOfAKind (prevPairValue: Rank) (hand: Card
list) =
49
let handCopy = removePairValue hand prevPairValue
match handCopy with
| [] -> (false, None)
| h when h.Length = 1 -> (true, Some prevPairValue)
| h when h.Length = 4 -> _isFourOfAKind (h.Head.Rank |>
fst) h.Tail
| _ -> (false, None)
50
4.1.10 API - Engine - Rank evaluation
Like with the hand evaluation module, the following function from
the rank evaluation module hosts the implementation of the equations
described at the end of subsubsection 3.7.2.
// Hand evaluation function - Variation of Cactus Kev's algorithm
that does not use
// lookup tables. This makes it slower than his algorithm, but
more efficient memory-wise.
let getRankingScore (ranking: HandRanking) (hand: Card list) =
// Only extract the ranked card from the hand
// E.g., if we get a pair of 2's, we only want to evaluate
// that pair of 2's
let hand =
match ranking with
| HighCard _ -> [hand |> List.maxBy (fun card ->
(card.Rank |> snd))]
| Pair rank
| ThreeOfAKind rank
| FourOfAKind rank -> hand
|> List.filter (fun card ->
(card.Rank |> fst) = rank.Value)
| TwoPair rankTup -> hand
|> List.filter (fun card ->
(card.Rank |> fst) =
(rankTup.Value |> fst) ||
(card.Rank |> fst) =
(rankTup.Value |> snd))
|> List.distinctBy (fun card ->
(card.Rank |> fst))
| _ -> hand
let primeProduct =
hand
|> List.map (fun card -> (card.Rank |> snd) |> int)
|> List.reduce (fun prod elem -> prod * elem)
let aceValue = 41
let kingValue = 37
let queenValue = 31
let jackValue = 29
51
let tensValue = 23
let ninesValue = 19
52
// Thus, we add the product of the highest possible straight
hand to the
// straight rank value.
let flushScore = straightScore + (aceValue * kingValue *
queenValue * jackValue * tensValue)
53
(fun prod
elem ->
prod *
elem))
54
let (isThreeOfAKind, threeOfAKindRank) = isThreeOfAKind hand
let (isTwoPair, twoPairRank) = isTwoPair hand
let (isPair, pairRank) = isPair hand
let (isHighCard, highCardRank) = (true, Some (getHighCard
hand))
let handRankings = [
(RoyalFlush royalFlushRank, isRoyalFlush)
(StraightFlush straightFlushRank, isStraightFlush)
(FourOfAKind fourOfAKindRank, isFourOfAKind)
(FullHouse fullHouseRank, isFullHouse)
(Flush flushRank, isFlush)
(Straight straightRank, isStraight)
(ThreeOfAKind threeOfAKindRank, isThreeOfAKind)
(TwoPair twoPairRank, isTwoPair)
(Pair pairRank, isPair)
(HighCard highCardRank, isHighCard)
]
handRankings
|> List.filter (fun rankings -> rankings |> snd = true)
|> List.map (fun rankings -> rankings |> fst)
55
A controller is responsible with setting up routes that a client can
hit using a particular HTTP method in order to apply different data
operations.
56
Regardless, this is how the controller (without the routes) looks
like:
[<ApiController>]
[<Route("[controller]")>]
type PokerController (logger : ILogger<PokerController>) =
inherit ControllerBase()
player.Bet <- 0
cpu.Bet <- 0
57
| (x, y) when x > y ->
let (player, cpu) = setWinner player cpu
player.Chips <- player.Chips + pot
As you will see with the getWinnerBasedOnScore function and the rest
of the functions in the controller, I am always passing the players, pot
and a boolean to the client in order to keep track of the state of the
game.
58
players <- p
(deck, players) |> this.Ok
This function simply sets the state of the game and returns the deck
and the players of the game. Those are constructed by calling the
”start” function, whose definition is the following:
let start (players: Player list) (playerName: string) =
let deck = generateDeck()
let players =
match players.Length = 0 with
59
players
|> List.map (fun p -> {p with Hand = []; Bet = 0;
Challenge = None; Score = 0})
In this function we simply create 2 players, deal them cards and com-
pute the scores of their hands.
60
The following endpoint is responsible with discarding cards from
the player’s and the CPU’s hand:
[<Route("game/discard")>]
[<HttpPost>]
member this.PostDiscard (discardInfo: DiscardInfo) =
let playerName = discardInfo.playerName
let cardsToDiscard = discardInfo.indices |> Seq.toList
Just like before, I had to define a custom type that defines the expected
JSON body:
type DiscardInfo = {
playerName: string
indices: seq<int>
}
61
When defining this type, I noticed something interesting that also
caused me some trouble - when a JSON array is sent from a JavaScript
client to an F# back-end, the back-end receives a sequence rather than
an array. In hindsight, this makes sense, as it is much safer to assume
that a collection (list, array, etc...) is a sequence (which it is), rather
than a specific collection.
62
The following GET endpoints involve all the actions that a player
can take given a challenge (betting, calling, folding, raising or going
all in):
[<Route("game/bet/{playerName}/{amount}")>]
[<HttpGet>]
member this.GetBet (playerName: string, amount: int) =
let player = getPlayerByName playerName
let cpu = getCPU player
let result =
match cpu with
// If the CPU folded, make the player the winner and add
// the pot to their total amount of chips.
| cpu when cpu.Challenge = Some Fold ->
let (player, cpu) = setWinner player cpu
player.Chips <- player.Chips + pot
63
Some Call ->
let halfPot = pot / 2
player.Bet <- 0
cpu.Bet <- 0
| _ ->
players <- [player; cpu]
64
totalPot <- totalPot + pot
result |> this.Ok
[<Route("game/raise/{playerName}/{amount}")>]
[<HttpGet>]
member this.GetRaise (playerName: string, amount: int) =
let player = getPlayerByName playerName
let cpu = getCPU player
let result =
match cpu.Challenge with
65
// If the challenge is anything other than a Fold or a
Call, the game continues
| _ ->
let roundOver = false
(players, pot, roundOver)
[<Route("game/call/{playerName}")>]
[<HttpGet>]
member this.GetCall (playerName: string) =
let player = getPlayerByName playerName
let cpu = getCPU player
[<Route("game/allIn/{playerName}")>]
[<HttpGet>]
member this.GetAllIn (playerName: string) =
let player = getPlayerByName playerName
let cpu = getCPU player
66
totalPot
cpu.Challenge
let result =
match previousCPUChallenge with
| Some AllIn -> getWinnerBasedOnScore player cpu pot
| _ ->
match cpu.Challenge with
[<Route("game/fold/{playerName}")>]
member this.GetFold (playerName: string) =
let player = getPlayerByName playerName
let cpu = getCPU player
67
let (player, pot) = player |> getPlayerChallenge
(Some Fold)
0
cpu.Bet
totalPot
cpu.Challenge
loser.Bet <- 0
loser.LostMatches <- loser.LostMatches + 1
winner, loser
68
4.2 The front-end
4.2.1 Setup - Global state management
One thing I really like to do when developing the front-end of an app
is to set up a global state management system.
Each component can have it’s own prop (short for property), which
is an attribute that the component can use when it is rendered. It is
similar to how you would define a ”div” in raw HTML:
<div class="some-class-name"></div>
69
Yet, as much of a great thing props can be for a component, they
are also the cause of one of the common problems in React front-end
development: prop drilling.
interface Props {
prop1: SomeObject,
prop2: any[],
prop3: string,
prop4: string,
prop5: string
}
console.log(prop2[0]);
70
Now, if we were to use this component in multiple areas, watch how
bloated the code will become:
interface SomeObject {
someProperty: any
}
This is where global state management is really useful and why I al-
ways like to set it up before commencing development on any front-end
app I am working on.
In order to setup global state, I made use of the Context API pro-
vided by React, which, in essence, creates 2 entities that implement
the observer pattern - the consumer and the provider.
71
ing actions.
Reducers are functions that do exactly this: given an action, mutate
the state with a payload (i.e. a string value, a boolean value, etc...).
Here I have defined the actions that I wanted the app to use to mutate
the global state:
export type Action =
| { type: "SET_DECK", payload: any }
| { type: "SET_PLAYER_INFO", payload: any }
| { type: "SET_CPU_INFO", payload: any }
| { type: "SET_PLAYERS", payload: any}
| { type: "RESET_GAME", payload: any }
As you can see, the action type is nothing other than a union type. It
contains objects with 2 properties - the action type, which will be used
in a switch statement to determine what action has been dispatched,
and the payload, which is simply the new value of the state.
Here is the definition of the game’s state, as well as the initial state of
the app:
export type GameState = {
deck: any;
playerInfo: any;
CPUInfo: any;
resetGame: boolean;
players: any;
};
72
import { Action } from "./actions";
import { GameState } from "./state";
As seen in this code snippet, this function returns a new object contain-
ing the previous state + the new state of the property being mutated.
73
This is analogous with the following F# code:
let gameReducer (state: GameState) (action: Action) =
match action.type with
| "SET_DECK" -> state with { deck = action.payload }
| "SET_PLAYER_INFO" -> state with { playerInfo =
action.payload }
...
After setting up the reducer and the state, I proceeded with setting
up the context of the app, which will in turn allow me to define the
provider of the app.
import React from 'react'
import { createContext, Dispatch, useContext, useReducer } from
'react'
import { Action } from './actions'
import { gameReducer } from './reducers'
import { GameState, initialGameState } from './state'
return (
<GameStateContext.Provider value={{state, dispatch}}>
{children}
</GameStateContext.Provider>
)
}
74
export { GameStateContext, GameStateProvider }
I also defined a custom useContext hook that will be useful when fetch-
ing the state and dispatch objects later in the component definitions.
The last step of the global state management system setup was to
wrap the root of the app under the custom context provider defined
earlier. This will make it such that the whole app consumes the context
given by the provider:
import React from "react"
function App() {
return (
<GameStateProvider>
<Game />
</GameStateProvider>
)
}
75
4.2.2 Setup - Interfaces
Here I have defined a few interfaces that were really useful later in
development:
export interface Card {
rank: number | string,
suit: number
}
76
4.2.3 Setup - Services
For the sake of organization and cleanliness, I have separated the func-
tions that make calls to the endpoints defined in the back-end in a
separate ”services” directory.
77
In any case, the following is the function responsible with sending
a GET request to the ”allIn” endpoint (I will only show and explain
this one as all services follow the same pattern):
import { Dispatch, SetStateAction } from "react"
import { apiConfig } from "../env/apiConfig"
import { Action } from "../stateManagement/actions"
import { mapPlayerHand, getHandRanking } from "../utils"
dispatch({
type: "SET_PLAYER_INFO",
payload: playerInfo
})
dispatch({
type: "SET_CPU_INFO",
payload: CPUInfo
})
})
.catch(err => console.log(err))
}
78
As seen in the snippet above, this service, along with the rest, are
responsible with fetching the data and mutating it with the dispatch
function defined in the global state management setup and / or calling
some callback function passed as parameter.
After testing this endpoint in Postman (an API testing client), I had
noticed that F# sends JSON back under a format analogous to the
following:
{
// record types
"item1": {
propertyOfRecordType1: valueOfProperty1,
},
79
Because of this, I had to define 2 helper functions, which made it
easier to map and retrieve the data from JSON bodies of this format:
const primes = [-1, -1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 27]
80
4.2.4 Setup - Components
interface Props {
hand: Card[];
hidden: boolean;
canDiscardViaUI: boolean;
discarded: boolean;
setCardsToDiscard: Dispatch<SetStateAction<number[]>>;
discardFn?: () => void;
}
function Hand({
hand,
hidden,
canDiscardViaUI,
81
discarded,
setCardsToDiscard,
discardFn,
}: Props) {
const initialHandDiscardStatus = {
0: false,
1: false,
2: false,
3: false,
4: false,
};
const [showDiscarded, setShowDiscarded] =
useState<boolean>(false);
const [handDiscardStatus, setHandDiscardStatus] =
useState<any>(
initialHandDiscardStatus
);
const [discardedCards, setDiscardedCards] = useState<any>([]);
useEffect(() => {
const indices = Object.entries(handDiscardStatus)
.filter(([_, value]: any) => value === true)
.map(([key, _]: any) => +key + 1)
.map(Number);
setCardsToDiscard(indices);
}, [handDiscardStatus]);
useEffect(() => {
if (!discarded)
setDiscardedCards(
hand.filter(
(_: any, index: number) =>
handDiscardStatus[index] === true
)
);
}, [handDiscardStatus]);
useEffect(() => {
const discardedCards = (
82
Object.values(handDiscardStatus) as boolean[]
).filter((status) => status === true);
setShowDiscarded(discardedCards.length !== 0);
}, [handDiscardStatus]);
useEffect(() => {
if (resetGame) {
setDiscardedCards([]);
setShowDiscarded(false);
setHandDiscardStatus(initialHandDiscardStatus);
}
}, [resetGame]);
return (
<div>
<HandComponent
cards={hand}
hidden={hidden}
style={handStyle}
onClick={(index: number) => {
if (!discarded && !hidden) {
setHandDiscardStatus({
...handDiscardStatus,
[index]: !handDiscardStatus[index],
});
}
}}
/>
{canDiscardViaUI ? (
<div>
<Button
disabled={discarded}
onClick={() => discardFn && discardFn()}
>
Discard!
</Button>
{showDiscarded ? (
<HandComponent
cards={discardedCards}
hidden={false}
style={discardedHandStyle}
onClick={() => {}}
83
/>
) : (
<></>
)}
</div>
) : (
<></>
)}
</div>
);
}
84
The following is the player component, which is nothing but a card
containing the player’s name, their total number of chips, the amount
the have bet, the hand of cards (shown earlier), and their hand’s high-
est rank, along with some buttons related to discarding and challenges
(bet, raise, fold, all in, call):
import { Dispatch, useEffect, useState } from "react";
import { makeStyles, Theme, createStyles } from
"@material-ui/core/styles";
import {
Avatar,
Button,
Card as CardComponent,
CardHeader,
CardContent,
CardActions,
Typography,
CardMedia,
} from "@material-ui/core";
interface Props {
name: string;
challenge: string | null;
betAmount: number;
totalChips: number;
wonMatches: number;
lostMatches: number;
highestRank: string | null;
hand: Card[];
85
isBot: boolean;
canDiscardViaUI: boolean;
isRoundFinished?: boolean;
discardFn?: (
playerName: string,
indices: number[],
setDiscarded: Dispatch<SetStateAction<boolean>>
) => void;
}
function Player({
name,
challenge,
betAmount,
totalChips,
wonMatches,
lostMatches,
highestRank,
hand,
isBot,
canDiscardViaUI,
isRoundFinished,
discardFn,
}: Props) {
const { state, dispatch } = useGameStateContext();
const { CPUInfo, resetGame } = state;
useEffect(() => {
if (resetGame) {
setDiscarded(false);
setBetted(false);
setOpenRaiseModal(false);
86
setOpenBetModal(false);
dispatch({
type: "RESET_GAME",
payload: false,
});
setCardsToDiscard([]);
}
}, [resetGame]);
87
{
label: "All In",
disabled: isBot || !discarded,
onClick: () => {
allIn(name, dispatch);
},
},
];
return (
<div>
<CardComponent className={classes.root}>
<CardHeader
avatar={
<Avatar aria-label={name}
className={classes.avatar}>
{name[0]}
</Avatar>
}
title={`${name} | Total chips: $${totalChips} |
Bet amount: $${betAmount}`}
subheader={`Won Matches: ${wonMatches} | Lost
Matches: ${lostMatches} | Highest rank: ${
highestRank !== null &&
(isBot === false || isRoundFinished ===
true)
? highestRank
: "Not visible yet"
}`}
/>
<CardMedia
className={classes.content}
children={
hand === undefined || hand.length === 0 ? (
<Typography
className={classes.noCardsToShow}
variant="body2"
>
No cards to show.
</Typography>
) : (
<Hand
88
hand={hand}
hidden={isBot}
canDiscardViaUI={canDiscardViaUI}
discarded={discarded}
setCardsToDiscard={setCardsToDiscard}
discardFn={() =>
discardFn &&
discardFn(
name,
cardsToDiscard,
setDiscarded
)
}
/>
)
}
/>
{isRoundFinished ? (
<></>
) : (
<CardActions disableSpacing
className={classes.content}>
{playerChallenges.map((challenge:
Challenge) => (
<Button
key={challenge.label}
disabled={challenge.disabled}
onClick={() => challenge.onClick()}
>
{challenge.label}
</Button>
))}
</CardActions>
)}
</CardComponent>
89
{name}'s challenge: {challenge}
</Typography>
</CardContent>
</CardComponent>
)}
<CustomModal
open={openBetModal}
setOpen={setOpenBetModal}
body={
<ChallengeModalContent
setOpen={setOpenBetModal}
minAmount={50}
maxAmount={totalChips}
type="Bet"
setBetted={setBetted}
/>
}
/>
<CustomModal
open={openRaiseModal}
setOpen={setOpenRaiseModal}
body={
<ChallengeModalContent
setOpen={setOpenBetModal}
minAmount={CPUInfo === null ? 50 :
+CPUInfo.bet + 1}
maxAmount={totalChips}
type="Raise"
/>
}
/>
</div>
);
}
90
This is how the player components look like:
91
Figure 4: The player’s hand after discarding the first 3 card’s in their
hand
92
Figure 6: The player confirming the bet amount and the opponent
raising
93
Figure 7: The player calling the opponent’s bet and winning
94
As you can also probably see, the total pot is not calculated prop-
erly, and it is because I wasn’t too bothered about it - my main goal
was to get the Poker game itself to work, so I overlooked the pot cal-
culation.
import "../../styles/App.css";
import {
Button,
createStyles,
makeStyles,
Theme,
TextField,
Typography,
} from "@material-ui/core";
function Game() {
const { state, dispatch } = useGameStateContext();
const { playerInfo, CPUInfo } = state;
95
const [gameButton, setGameButton] = useState<GameButton>({
text: "Start New Game",
textColor: "white",
bgColor: startGameColor,
active: true,
});
useEffect(() => {
if (playersStatus !== null && playerInfo !== null &&
CPUInfo !== null) {
const playerWonMatchesGuard =
playersStatus.playerInfo.wonMatches !==
playerInfo?.wonMatches;
const playerLostMatchesGuard =
playersStatus.playerInfo.lostMatches !==
playerInfo?.lostMatches;
const CPUWonMatchesGuard =
playersStatus.CPUInfo.wonMatches !==
CPUInfo?.wonMatches;
const CPULostMatchesGuard =
playersStatus.CPUInfo.lostMatches !==
CPUInfo?.lostMatches;
if (
gameStarted &&
(playerWonMatchesGuard ||
playerLostMatchesGuard ||
CPUWonMatchesGuard ||
CPULostMatchesGuard)
) {
96
setGameStarted(false);
setGameButton({
text: "Play again?",
textColor: "White",
bgColor: playAgainColor,
active: true,
});
setRoundFinished(true);
}
}
}, [playerInfo, CPUInfo]);
return (
<div className="App">
<div>
<Typography variant="h2">
Five-Card Draw | CMP5344 | Andrei-Alin Murjan
</Typography>
</div>
<TextField
disabled={gameStarted}
placeholder="What's your name, player?"
type="text"
variant="outlined"
onChange={(e) => setPlayerName(e.target.value)}
/>
<Button
disabled={!gameButton.active || playerName === ""}
onClick={() => {
const playerHasNoChips =
playerInfo !== null && playerInfo.chips ===
0;
const CPUHasNoChips =
CPUInfo !== null && CPUInfo.chips === 0;
resetGameStatus();
setGameButton({
...gameButton,
active: false,
});
97
startGame(
playerName === "" ? "No Name" : playerName,
setGameStarted,
playerHasNoChips || CPUHasNoChips,
dispatch,
setPlayersStatus
);
setRoundFinished(false);
setPlayersStatus(null);
}}
>
{gameButton.text}
</Button>
<Player
name={playerName === "" ? "No Name" : playerName}
challenge={
playerInfo === null || playerInfo.challenge ===
null
? null
: Object.entries(playerInfo.challenge.value)
.filter(([_, value]): any => value ===
true)
.map(([key, _]): any =>
key.slice(2))[0]
}
betAmount={playerInfo === null ? 0 :
+playerInfo.bet}
totalChips={playerInfo === null ? 500 :
+playerInfo.chips}
wonMatches={playerInfo === null ? 0 :
+playerInfo.wonMatches}
lostMatches={playerInfo === null ? 0 :
+playerInfo.lostMatches}
highestRank={
playerInfo === null ? null :
playerInfo.highestRank
}
hand={playerInfo === null ? [] : playerInfo.hand}
isBot={false}
isRoundFinished={roundFinished}
canDiscardViaUI={true}
98
discardFn={(
playerName: string,
indices: number[],
setDiscarded: Dispatch<SetStateAction<boolean>>
) => {
discardCards(playerName, indices, setDiscarded,
dispatch);
}}
/>
<Player
name="CPU"
challenge={
CPUInfo === null || CPUInfo.challenge === null
? null
: Object.entries(CPUInfo.challenge.value)
.filter(([_, value]): any => value ===
true)
.map(([key, _]): any =>
key.slice(2))[0]
}
betAmount={
CPUInfo === null || CPUInfo.challenge === null
? 0
: +CPUInfo.bet
}
totalChips={CPUInfo === null ? 500 :
+CPUInfo.chips}
wonMatches={CPUInfo === null ? 0 :
+CPUInfo.wonMatches}
lostMatches={CPUInfo === null ? 0 :
+CPUInfo.lostMatches}
highestRank={CPUInfo === null ? null :
CPUInfo.highestRank}
hand={CPUInfo === null ? [] : CPUInfo.hand}
isBot={gameStarted}
isRoundFinished={roundFinished}
canDiscardViaUI={false}
/>
</div>
);
}
99
export default Game;
This is how the web app looks after the game component’s definition:
100
Figure 9: Start of Poker game
101
5 Testing
I chose TickSpec + Expecto as my testing framework of choice.
Here’s how the entrypoint file of the testing framework looks like:
module Program
open System.Reflection
open System.IO
open Expecto
102
let featureTest (resourceName: string) =
resourceName
|> featureFromEmbeddedResource
|> testListFromFeature
[<Tests>]
let cardRankingsTests = featureTest
"features/CardRankings.feature"
[<EntryPoint>]
let main args =
runTestsInAssembly defaultConfig args
TickSpec creates step definitions (i.e. Given, When and Then steps)
by making use of the ”.feature” file representing the specifications of
the system and the stream reader used to read from said file.
It then passes it to the ”testListFromFeature” function, which maps
the step definitions into a list and passes them to Expecto.
[<Given>]
member __.``The given step``() =
initialState + 1
[<When>]
member __.``The when step``(newState: int) =
myAction newState
[<Then>]
member __.``The then step`` (newState: int) =
let passed = newState = 5
Expect.isTrue passed
The only tests I have set up were related to the hand ranking evaluation
103
functions. I figured that those were the only ones needing automated
tests, since challenging players and basic card operations can easily be
tested manually.
The tests were also pretty handy when I went back and refactored
the hand evaluation engine and got rid of some code smells.
The following are the tests I wrote in order to validate my hand eval-
uator implementation.
module CardRankingsStepsDefinition
open Cardistry.Model
open Engine.Model
open Engine.RankEval
open TickSpec
open Expecto
let player = {
Name = "Test";
Hand = [];
Score = 0;
Chips = 500;
Bet = 0;
WonMatches = 0;
LostMatches = 0;
HighestRank = None;
Challenge = None
}
type HighCardSteps() =
[<Given>]
104
member __.``The player has a hand of 5 cards with no
ranking``() =
player
|> initializePlayerHand [
{ Suit = Clubs; Rank = (Common 10, 23) }
{ Suit = Clubs; Rank = (King 14, 37) }
{ Suit = Diamonds; Rank = (Common 9, 19) }
{ Suit = Diamonds; Rank = (Common 3, 3) }
{ Suit = Spades; Rank = (Common 4, 5) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is High Card`` (player: Player)
=
let passed =
match player.HighestRank with
| Some (HighCard _) -> true
| _ -> false
Expect.isTrue passed
type PairSteps() =
[<Given>]
member __.``The player has a hand of 5 cards with 2 cards
whose values are identical``()=
player
|> initializePlayerHand [
{ Suit = Clubs; Rank = (Common 10, 23) }
{ Suit = Clubs; Rank = (King 14, 37) }
{ Suit = Diamonds; Rank = (King 14, 37) }
{ Suit = Diamonds; Rank = (Common 3, 3) }
{ Suit = Spades; Rank = (Common 4, 5) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is Pair`` (player: Player) =
let passed =
match player.HighestRank with
| Some (Pair _) -> true
| _ -> false
Expect.isTrue passed
105
type TwoPairSteps() =
[<Given>]
member __.``The player has a hand of 5 cards with 2 pairs``()
=
player
|> initializePlayerHand [
{ Suit = Clubs; Rank = (Common 10, 23) }
{ Suit = Clubs; Rank = (King 14, 37) }
{ Suit = Diamonds; Rank = (King 14, 37) }
{ Suit = Diamonds; Rank = (Common 4, 5) }
{ Suit = Spades; Rank = (Common 4, 5) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is Two Pair`` (player: Player) =
let passed =
match player.HighestRank with
| Some (TwoPair _) -> true
| _ -> false
Expect.isTrue passed
type ThreeOfAKindSteps() =
[<Given>]
member __.``The player has a hand of 5 cards with 3 cards
whose values are identical``() =
player
|> initializePlayerHand [
{ Suit = Clubs; Rank = (Common 10, 23) }
{ Suit = Clubs; Rank = (King 14, 37) }
{ Suit = Diamonds; Rank = (King 14, 37) }
{ Suit = Hearts; Rank = (King 14, 37) }
{ Suit = Spades; Rank = (Common 4, 5) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is Three of a Kind`` (player:
Player) =
let passed =
match player.HighestRank with
106
| Some (ThreeOfAKind _) -> true
| _ -> false
Expect.isTrue passed
type Straight() =
[<Given>]
member __.``The player has a hand of 5 cards whose values are
consecutive``() =
player
|> initializePlayerHand [
{ Suit = Hearts; Rank = (Common 9, 19) }
{ Suit = Clubs; Rank = (Common 10, 23) }
{ Suit = Diamonds; Rank = (Jack 12, 29) }
{ Suit = Spades; Rank = (Queen 13, 31) }
{ Suit = Clubs; Rank = (King 14, 37) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is Straight`` (player: Player) =
let passed =
match player.HighestRank with
| Some (Straight _) -> true
| _ -> false
Expect.isTrue passed
type Flush() =
[<Given>]
member __.``The player has a hand of 5 cards whose suits are
identical, regardless of value``() =
player
|> initializePlayerHand [
{ Suit = Clubs; Rank = (Common 8, 17) }
{ Suit = Clubs; Rank = (Common 10, 23) }
{ Suit = Clubs; Rank = (Jack 12, 29) }
{ Suit = Clubs; Rank = (Queen 13, 31) }
{ Suit = Clubs; Rank = (King 14, 37) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is Flush`` (player: Player) =
107
let passed =
match player.HighestRank with
| Some (Flush _) -> true
| _ -> false
Expect.isTrue passed
type FullHouseSteps() =
[<Given>]
member __.``The player has a hand of 5 cards with a pair and
a three of a kind``() =
player
|> initializePlayerHand [
{ Suit = Clubs; Rank = (Common 8, 17) }
{ Suit = Hearts; Rank = (Common 8, 17) }
{ Suit = Diamonds; Rank = (Jack 12, 29) }
{ Suit = Spades; Rank = (Jack 12, 29) }
{ Suit = Clubs; Rank = (Jack 12, 29) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is Full House`` (player:
Player) =
let passed =
match player.HighestRank with
| Some (FullHouse _) -> true
| _ -> false
Expect.isTrue passed
type FourOfAKindSteps() =
[<Given>]
member __.``The player has a hand of 5 cards with 4 cards
whose values are identical``() =
player
|> initializePlayerHand [
{ Suit = Clubs; Rank = (Common 8, 17) }
{ Suit = Clubs; Rank = (Jack 12, 29) }
{ Suit = Diamonds; Rank = (Jack 12, 29) }
{ Suit = Hearts; Rank = (Jack 12, 29) }
{ Suit = Spades; Rank = (Jack 12, 29) }
]
|> populatePlayerHighestRank
108
[<Then>]
member __.``The player's rank is Four of a Kind`` (player:
Player) =
let passed =
match player.HighestRank with
| Some (FourOfAKind _) -> true
| _ -> false
Expect.isTrue passed
type StraightFlushSteps() =
[<Given>]
member __.``The player has a hand of 5 cards without an Ace,
whose values are consecutive and have the same suit``() =
player
|> initializePlayerHand [
{ Suit = Hearts; Rank = (Common 9, 19) }
{ Suit = Hearts; Rank = (Common 10, 23) }
{ Suit = Hearts; Rank = (Jack 12, 29) }
{ Suit = Hearts; Rank = (Queen 13, 31) }
{ Suit = Hearts; Rank = (King 14, 37) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is Straight Flush`` (player:
Player) =
let passed =
match player.HighestRank with
| Some (StraightFlush _) -> true
| _ -> false
Expect.isTrue passed
type RoyalFlushSteps() =
[<Given>]
member __.``The player has a hand of 5 cards with an Ace,
whose values are consecutive and have the same suit in
their hand``() =
player
|> initializePlayerHand [
{ Suit = Hearts; Rank = (Ace 15, 41) }
{ Suit = Hearts; Rank = (King 14, 37) }
109
{ Suit = Hearts; Rank = (Queen 13, 31) }
{ Suit = Hearts; Rank = (Jack 12, 29) }
{ Suit = Hearts; Rank = (Common 10, 23) }
]
|> populatePlayerHighestRank
[<Then>]
member __.``The player's rank is Royal Flush`` (player:
Player) =
let passed =
match player.HighestRank with
| Some (RoyalFlush _) -> true
| _ -> false
Expect.isTrue passed
110
6 Conclusion
Although there are many things that can be improved with this Five-
Card Draw Poker game, I am pretty happy with how it turned out and
with my implementation.
111
References
CactusKev. Cactus kev’s poker hand evaluator. Available from: http:
//suffe.cool/poker/evaluator.html.
112