0% found this document useful (0 votes)
14 views112 pages

Example Sample Logbook 2

coursework

Uploaded by

adilhussain606
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
14 views112 pages

Example Sample Logbook 2

coursework

Uploaded by

adilhussain606
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 112

Five-Card Draw Poker

CMP5344 Discrete Mathematics and Declarative


Programming Coursework

Andrei-Alin Murjan
May 31, 2021

1
Contents
1 Introduction 4

2 Environment setup and initial plans 4

3 Planning and Design 6


3.1 Target audience . . . . . . . . . . . . . . . . . . . . . . 6
3.2 The rules of the Five-Card Draw Poker game . . . . . . 6
3.3 The deck of cards . . . . . . . . . . . . . . . . . . . . . 6
3.4 The player(s) . . . . . . . . . . . . . . . . . . . . . . . 7
3.5 Dealing cards . . . . . . . . . . . . . . . . . . . . . . . 9
3.6 Discarding cards . . . . . . . . . . . . . . . . . . . . . 10
3.7 Evaluating a hand’s strength . . . . . . . . . . . . . . . 11
3.7.1 Hand ranking and new card data models . . . . 12
3.7.2 The hand evaluation algorithm . . . . . . . . . 12
3.8 Challenging a player . . . . . . . . . . . . . . . . . . . 23

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.

2 Environment setup and initial plans


Before starting to work on the project, I knew that, first and foremost,
I had to gain an understanding of how my game engine will work, lead-
ing me into wanting to create a CLI prototype at first, where I could
play around with some ideas that came into my head and, later on,
mold those ideas such that they fit in the context of the final product.

My development environment involved using VSCode, as the editor


of my choice and using .NET Core as the framework used to build F#
applications.

Eventually, after I finished tinkering with the prototype, I wanted to


transfer the game engine mechanics into the context of a web-based
application, using ASP.NET Core for my back-end, using F# as the
functional programming language, and React as my front-end, using
TypeScript as my language of choice, where I only stuck to applying
declarative programming principles.

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.

3.2 The rules of the Five-Card Draw Poker game


The Five-Card Draw Poker game is one of the simplest forms of Poker
that exists.
In short, the game can be described as follows:
1. 5 cards are dealt to each player.
2. Each player can discard nothing or their entire hand.
3. After discarding, each player is dealt as many cards as they dis-
carded.
4. Afterwards, players can choose to challenge other players by bet-
ting, raising, folding, calling or going all in.
5. The player with the strongest hand wins.

3.3 The deck of cards


In a game of Five-Card Draw, a 52-card deck, without Jokers, is used.
A card has a suit and a rank. The suits are Clubs, Diamonds, Hearts
and Spades, and the ranks are from 2 to Ace.

Since the ranks from 2 to 10 are natural numbers, we can represent


them in a set like the following:

Let Common = {2, 3, 4, . . . , 10} (1)

6
The Aces, Kings, Queens and Jacks can be represented in the fol-
lowing way:

Let Ace := {x | x = 15 since the Ace is the highest rank }


Let King := {x | x = 14}
Let Queen := {x | x = 13}
Let Jack := {x | x = 12}

With this in mind, we can represent a card as follows:


Rank : Ace ∪ King ∪ Queen ∪ Jack ∪ Common
Suit : Clubs ∪ Diamonds ∪ Hearts ∪ Spades
Card ={Suit: Suit, Rank: Rank}
It is also important that the deck of cards is shuffled before each Poker
match starts.
To shuffle a deck means to iterate through the cards and move each
card from its current position to a random position in the deck.

3.4 The player(s)


In a game of Poker, there are 5 elements that the player model needs
to have in order for the game to be functional:

1. A player needs to have a hand of 0 cards minimum and 5 cards


maximum.
Their hand can be empty whenever they discard all 5 cards in
their hand or they have not been dealt cards yet.
Their hand can be full when they have been dealt five cards.

2. The strength of their hand. This could be represented by a score.


The higher the score, the stronger the hand.

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:

1. The player’s name

2. The number of matches the player won

3. The number of matches the player lost

4. Their hand’s highest rank, that can be used to let the user know
how strong their hand is.

With this information, we can represent a player as follows:

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)
}

3.5 Dealing cards


To deal a card to a player means to take a card from the top of the
deck, giving it to a player and removing it from the deck.
This mechanic can be described using the following predicate logic:

Let c := The deck of cards is created


Let s := The deck of cards is shuffled
Let d := The cards are dealt
Let l := The player should have 5 cards in their hand

P = c ∧ s =⇒ (d =⇒ l)

Analogously, card dealing can be represented the following business


logic (using the Gherkin syntax):

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

Then The p l a y e r s h o u l d have 5 c a r d s i n t h e i r hand

3.6 Discarding cards


Let N be the number of cards. To discard N cards means to exchange
the N cards with the top N cards from the deck.
The card discarding mechanic can be represented through the follow-
ing predicate logic:

Let h := The player has a hand of 5 cards


Let c := The player chose to discard a number of cards greater than 0
Let d := The chosen cards are discarded
Let x := The same number of cards is dealt to the player

P = h =⇒ (c =⇒ d ∧ x)

This mechanic can also be represented through the following business


logic:

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

Then The chosen c a r d s a r e d i s c a r d e d

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

3.7 Evaluating a hand’s strength

There are various ways to evaluate a hand’s strength. An algorithm I


used as inspiration to come up with my own hand evaluator is Cactus
Kev’s hand evaluator (CactusKev), which involves using a precom-
puted array of unique products obtained by multiplying the prime
numbers correspondent to each card (see table below) in all distinct
hand combinations.

Rank 2 3 4 5 6 7 8 9 10 Jack Queen King Ace


Prime 2 3 5 7 11 13 17 19 23 29 31 37 41
Traditionally, the way a Poker hand’s strength is evaluated is based on
the ranking the hand has, where a High Card is the weakest ranking,
and the Royal Flush is the strongest ranking.

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 }

Since my hand evaluation algorithm was also based on rank-correspondent


prime numbers, I had to do some modifications to the card data model:
P rime : N0
Card ={Suit: Suit, Rank: Rank x Prime}
The reason why the rank is now a tuple (cartesian product) made
of the card’s rank and the corresponding prime is that it makes pat-
tern matching and data handling a bit simpler, rather than having a
separate ”Prime” field.

3.7.2 The hand evaluation algorithm


First and foremost, I had to define what each hand ranking involved.

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:

HighCard(h) = max(h), ∀h : seqCard (2)


S c e n a r i o : High Card
Given The p l a y e r has a hand o f 5 c a r d s
with d i f f e r e n t r a n k s and a v a r i e t y o f
suits

Then The p l a y e r ' s hand r a n k i n g i s


high card

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

Then The p l a y e r ' s rank i s p a i r

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

Then The p l a y e r ' s rank i s Two P a i r

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

Then The p l a y e r ' s rank i s Three o f a Kind

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

Then The p l a y e r ' s rank i s Flush

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

Then The p l a y e r ' s rank i s F u l l House

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

Then The p l a y e r ' s rank i s Four o f a Kind

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

Then The p l a y e r ' s rank i s S t r a i g h t Flush

10. Royal Flush.


A royal flush is a straight flush where the highest card rank is an
Ace. An easy way to compute this is to compute the product of
primes and check if it is equal to 31367009 (23 * 29 * 31 * 37 * 41).

∀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

Then The p l a y e r ' s rank i s Royal Flush

After defining how each hand ranking is computed, I had to find a


way to compute how strong the hand is. Identifying a hand ranking is
useless if it cannot be compared to other hand rankings. Thus, I had
to find a way to compute the score of the hand.
By taking a few days to brainstorm some ideas, I came up with a work-
ing algorithm inspired by the Fibonacci sequence and other recurring
sequences.
Although it is not a recursive algorithm in its own right, it has the
property of using previous score definitions.
Below you’ll find the algorithm in equation form:

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.

Since prime numbers always generate unique products amongst other


prime numbers, this approach is feasible and works great. This is true,
as the Fundamental Theorem of Arithmetic, which states that any
positive integer greater than 1 can be written as a product of prime
numbers, holds.

3.8 Challenging a player


As mentioned in subsection 3.4, there are 5 challenges that a player
can make.

1. Betting: To bet an amount simply means to subtract that amount


from the total number of chips a player has and put it in the pot.
In order for a bet to be placed, it is required that it represents
the first challenge in a round (i.e. you cannot simply bet af-
ter someone raises, calls, folds or go all in - betting is a starter
action).
Scenario : Betting

Given The p l a y e r has enough c h i p s

When The p l a y e r ' s c h a l l e n g e i s t h e f i r s t


c h a l l e n g e i n t he round
And The p l a y e r b e t s 50 USD

Then The p l a y e r ' s b e t t i n g amount

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

2. Folding: To fold means to begin a challenge or answer to one by


giving up. When a player folds, they lose the round and all the
chips they bet. Since the player that folded loses the round, the
challenging ends.
Scenario : Folding

Given The pot t o t a l s t o 50 USD

When The p l a y e r ' s c h a l l e n g e i s a Fold


And The p l a y e r g i v e s up t he pot t o th e opponent

Then The opponent s h o u l d have 50 USD more c h i p s


And The p l a y e r s s t o p c h a l l e n g i n g t h e m s e l v e s

3. Raising: To raise means to answer to a bet by betting a larger


amount. Raising is simultaneously an answer and a question to
the betting challenge, since, after a raise, the opponent has to
decide on a new challenge. E.g. If player A bets and player B
raises, then player A has to decide on a new challenge, as player
B is the one asking the question now.
Additionally, players can chain raises as much as they can afford.
The round does not end if 2 players’ challenges match.
Scenario : Raising

Given The p l a y e r has enough c h i p s


And The opponent ' s c h a l l e n g e was
not a Fold o r a C a l l
And The p l a y e r ' s c h a l l e n g e i s t h e r e s p o n s e
t o t he opponent ' s c h a l l e n g e

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

Then The p l a y e r r a i s e s t he pot by t h e


amount th e have i n p u t t e d

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

Given The p l a y e r has a t l e a s t 1 USD


worth o f c h i p s

When The p l a y e r ' s c h a l l e n g e i s A l l In

Then The b e t amount i s e q u a l t o th e


player ' s chips
And The p l a y e r ' s c h i p s i s e q u a l t o 0
And The opponent ' s o n l y o p t i o n s a r e
t o Fold o r C a l l

5. Calling: To call means to answer to a betting challenge by betting


the same amount as the opponent (e.g. if player A bet $50 and
player B calls, then player B also bet $50). Additionally, when
someone calls, the challenging ends.
Scenario : Calling

Given The p l a y e r has enough c h i p s


And The opponent ' s c h a l l e n g e was not a Fold
And The p l a y e r ' s c h a l l e n g e i s t h e r e s p o n s e
t o t he opponent ' s c h a l l e n g e

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

Then The p l a y e r ' s be t amount i s e q u a l t o t he


p r e v i o u s p l a y e r ' s amount
And The p l a y e r s s t o p c h a l l e n g i n g t h e m s e l v e s

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.

4.1 The back-end


4.1.1 Data modelling - The card
As previously discussed, the card model will have to hold a rank and
a suit. The rank will be a tuple containing the card’s value (the ac-
tual rank) and its associated prime number that we’ll use later for
evaluating the hand.
// Cards can be of Hearts,
// Clubs, Spades and Diamonds
type Suit =
| Clubs
| Diamonds
| Hearts
| Spades

type Prime = int

// 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"

// A card is can be represented


// as a record type: A card has a suit and a rank
type Card = {
Suit: Suit
Rank: Rank * Prime

27
}

4.1.2 Data modelling - The hand ranking


When we get to evaluating a hand, we’ll make use of a hand ranking
model in order to do some pattern matching.
type HandRanking =
| HighCard of (Rank * Suit) option
| Pair of Rank option
| TwoPair of (Rank * Rank) option
| ThreeOfAKind of Rank option
| Straight of Card list option
| Flush of Suit option
| FullHouse of (Rank * Rank) option
| FourOfAKind of Rank option
| StraightFlush of (Rank list * Suit) option
| RoyalFlush of (Rank list * Suit) option

The reason for creating the HandRanking type as a discriminated union


and not a regular union type is that I wanted to be able to extract a
HandRanking’s wrapped value in pattern matching expressions.
Since some rankings depend on others when determining what rank-
ing a hand has, it is useful to have access to the wrapped value of a
HandRanking.

4.1.3 Data modelling - The challenge


As previously stated, there are 5 challenges a player can engage in:
betting, calling, raising, folding and going all in.
type PlayerChallenge = Bet | Call | Raise | Fold | AllIn

4.1.4 Data modelling - The player

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.

Logically, it wouldn’t make much sense to construct a new player with


different field values. Most game mechanics involve mutating the state
of the player, thus making in-place operations on it.

Then again, having mutable fields leads to the creation of impure func-
tions, which beats the point of the declarative paradigm.

I chose to stick to the logical representation of a player, as it makes the


code more readable and makes more sense. To me it is more important
that the code I write makes sense for me and the people that read and
contribute to it.

Strictly sticking to a paradigm is good as it reduces bugs and ensures


consistency, but if the code is unreadable, it can cause more bugs or in-
crease technical debt (e.g. Developer A will have to spend time reverse
engineering the unreadable code written by Developer B and refactor
it such that it makes sense before improving it / adding to it).

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 swap function does an in-place swap between values at 2 given


indices.

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.

All the operations have been defined under a ”CardOps” module.

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
}
|]

Since lists are immutable (random access is not a characteristic of


linked lists), I had to create the deck as an array since I wanted to
shuffle it.
In order to shuffle the created deck, I needed a random number gen-
erator such that I can generate a random index to execute the swap
function on.
let generateDeck(): Model.Card list =
// Random number generator - used for shuffling the cards
let rand = new Random()

initialDeck
|> Array.iteri (fun i _ ->
let randIndex = rand.Next(i, Array.length initialDeck)
Utils.swap i randIndex initialDeck)

List.ofArray initialDeck

The generateDeck function is an implementation of the Fisher-Yates


shuffle algorithm (Wikipedia), which is the simplest form of shuffling.
It involves generating a random permutation of a finite sequence.

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)

let getFormattedCardTuple (card: Model.Card) =


match (card.Rank |> fst) with
| (Model.Ace v) -> (Some (Model.Ace v), None)
| (Model.Jack v) -> (Some (Model.Jack v), None)
| (Model.Queen v) -> (Some (Model.Queen v), None)
| (Model.King v) -> (Some (Model.King v), None)
| (Model.Common v) -> (None, Some v)

let getRankValue (rank: Model.Rank) =


match rank with
| (Model.Ace v) -> v
| (Model.Jack v) -> v
| (Model.Queen v) -> v
| (Model.King v) -> v
| (Model.Common v) -> v

let formatHand (hand: Model.Card list) =


hand
|> List.map (fun card ->
match (getFormattedCardTuple card) with
| (None, value) -> sprintf "%d of %A" value.Value card.Suit
| (kind, None) -> sprintf "%A of %A" kind.Value card.Suit
| _ -> sprintf "")

32
4.1.6 API - Engine - Utils

I created 2 utility functions that were really useful in the card-related


operations.
module Utils =
// Remove a particular card from the deck / hand
let rec removeCardFromCollection (card: Card) (collection:
Card list) =
match collection with
| firstCard::restOfDeck when firstCard = card -> restOfDeck
| firstCard::restOfDeck ->
firstCard::removeCardFromCollection card restOfDeck
| _ -> []

// Remove the top card from the deck


let removeTopCardFromDeck (deck: Card list) =
match deck with
| _::restOfDeck -> restOfDeck
| _ -> []

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.

4.1.7 API - Engine - Dealing

Under a module named ”Dealing”, in the ”Engine” namespace, I cre-


ated card dealing and discarding functions.

The following is the single card dealing function:


// In order to deal card, the top card must be added
// to the player's hand and removed from the deck
let dealCard (player: Player) (deck: Card list) =
player.Hand <- deck.Head::player.Hand
removeTopCardFromDeck deck // Also retrieves the new deck,
// with the top card removed

As suggested by the comments, dealing a card means taking it from

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) =

match steps with


| step when step = numSteps -> deck
| _ ->
let deck = dealCard player deck
deck |> _dealCards (steps + 1) numSteps player

let rec dealCards (players: Player list) (deck: Card list) =


let (currStep, numSteps) = (0, 5)
match players with
| p::ps -> _dealCards
currStep
numSteps
p
deck
|> dealCards ps
| _ -> deck

In order to deal a card, I am keeping track of how many iterations the


function should recur for. Every iteration a card is dealt to the player,
the deck is updated and a new recursive call is made.

In the second function, I am simply recursively iterating through the


players and dealing them cards with the function described above.

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.

The following functions are responsible with discarding cards:


let _discardCards
(deck: Card list)
(player: Player)
(choices: int list) =

if choices |> List.min = 0 then deck


else
match (choices |> List.max) with
| 6 ->
player.Hand <- []
_dealCards 0 5 player deck
| _ ->
[ for choice in choices do yield (player.Hand |>
Array.ofList).[choice - 1] ]
|> List.iter (fun cardToDiscard ->
let newHand =
player.Hand
|> List.filter (fun card -> card <>
cardToDiscard)
player.Hand <- newHand)
_dealCards 0 choices.Length player deck

35
let discardCards
(player: Player)
(options: int list)
(isCPU: bool)
(deck: Card list) =

let rand = new Random()

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

match (choices |> List.filter (fun choice -> choice >


6)).Length with
| 0 ->
choices |> _discardCards deck player
| _ ->
printfn "Invalid option(s)"
deck
discardChoice()

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.

4.1.8 API - Engine - Actions


The ”Actions” module hosts all the mechanics related to the player
challenges.

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)

match invalidBetConditions with


| (false, false, false) -> (bet, "")
| (true, false, false) -> (-1, "You must bet an amount over
$0 (e.g. $50)")
| (false, true, false) -> (-1, sprintf "You must bet at least
$%d" minimumAmount)
| (false, false, true) -> (-1, "You cannot bet more than you
can afford")
| _ -> (-1, "An error occurred")

let private getCPUBet (previousBet: int) (totalChips: int) =


if previousBet > totalChips then totalChips
else (rand.Next (totalChips - previousBet)) + previousBet + 1

37
let private setBet (player: Player) (amount: int) =
player.Bet <- amount
player.Chips <- player.Chips - amount

let private getPotAfterBet


(player: Player)
(amount: int)
(previousBet: int)
(minimumAmount: int)
(pot: int)
(isUser: bool) =

match isUser with


| true ->
let (b, err) = getPlayerBet amount player.Chips
minimumAmount
if err <> "" then (pot, err)
else
setBet player b
(b + pot, "")
| false ->
if player.Chips <> 0 then
let b = getCPUBet previousBet player.Chips
setBet player b
(b + pot, "")
else (pot, "")

38
The following is the function responsible with dispatching the bet
action:
let private bet
(player: Player)
(amount: int)
(_pot: int)
(isUser: bool) =

let (pot, err) = getPotAfterBet


player
amount
0
50
_pot
isUser

if err <> "" then


printfn "%s" err
player, _pot
else
player.Challenge <- Some Bet
player, pot

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

The following function implements the raise mechanic. Raising is just


like betting, except that we care about what the previous bet was.
Also, the minimum amount should at least be equal to the previous
bet + 1.
let private raise
(player: Player)
(amount: int)
(previousBet: int)
(_pot: int)
(isUser: bool) =

let (pot, err) = getPotAfterBet


player
amount
previousBet
(previousBet + 1)
_pot
isUser

if err <> "" then


printfn "%s" err
player, _pot
else
player.Challenge <- Some Raise
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

let private allIn (player: Player) (pot: int) =


setBet player player.Chips
player.Challenge <- Some AllIn
player, pot + player.Chips

Calling is nothing more than matching the opponent’s bet amount. If


the player’s number of chips is less than what the opponent bet, then
calling would be the equivalent of going all in. As such, going all in
simply means for a player to bet all the chips they own.

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))

match (opt, currentChallenge.IsSome) with


| (Some Bet, false) -> bet player amount 0 isUser
| (Some Call, true) -> call player previousBet pot
| (Some Raise, true) -> raise player amount previousBet pot
isUser
| (Some AllIn, _) -> allIn player pot
| (Some Fold, _) -> fold player pot
| _ ->
printfn "Invalid option."
player, pot

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) =

let challenges = [Some Call; Some Fold]


let challenge =
challenges
|> List.item (rand.Next (challenges.Length))
cpu
|> getCPUChallengeExplicit
challenge
0
previousBet
pot
(Some AllIn)

43
4.1.9 API - Engine - Hand evaluation

The following module implements the equations defined in subsubsec-


tion 3.7.2:
module HandEval =

// Function used to check if the element we are currently


// checking is the same as the one at a given index
let isFromSameIndex (hand: Card list) (card: Card) (index:
int) =
card = (hand |> List.item index)

/// High Card


let getHighCard (hand: Card list) =
hand
|> List.maxBy (fun card -> card.Rank |> snd)
|> (fun card -> ((card.Rank |> fst), card.Suit))

/// 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

// If none of the above patterns match, but we can


keep iterating through
// the hand, we'll do so
| h -> _checkPairForIter
h
card
(iteration + 1)

// Method that actively checks for pairs, given a starting


iteration
let rec private _isPair (hand: Card list) (card: Card)
(iteration: int) =
// Match the hand of cards with the following patterns:
match hand with
// If we found a pair, we'll return a tuple containing true
// and the value of the card under an option type (reason
-> if there isn't a pair,
// we can use "None" as the value)
| h when (_checkPairForIter h card iteration) -> (true,
Some (card.Rank |> fst))

// If we didn't find a pair and we finished the iteration,


return a tuple containing
// false and a None value
| h when ((_checkPairForIter h card iteration) |> not) &&
(iteration = (h.Length - 1)) -> (false, None)

// If none of the above patterns match, but we can still


iterate over the hand,
// we'll do so.
| h -> (_isPair h (h |> List.item (iteration + 1))
(iteration + 1))

// Default type -> No pair

45
| _ -> (false, None)

// We'll check if we have a pair starting from iteration 0


let isPair (hand: Card list) = _isPair hand hand.Head 0

/// Pair operations (two pair, three of a kind, four of a


kind, full house)
// Function that removes cards whose values are equal to a
given value.
// E.g. Rank-wise, 2 of Hearts is equal to 2 of Spades.
let removePairValue (hand: Card list) (value: Rank) =
let handLength = hand.Length
hand
|> List.filter (fun card -> (card.Rank |> fst) <> value)
|> (fun h ->
match h.Length with
| l when l = handLength - 1 -> []
| _ -> h)

let pairOps operation (hand: Card list) =


// Create a shallow copy of the hand
let handCopy = hand

// Get the pair tuple from the hand


let (hasPair, value) = isPair handCopy

if hasPair then operation value.Value handCopy


else (false, None)

// Two Pair
let isTwoPair (hand: Card list) =
// Create a shallow copy of the hand
let handCopy = hand

// Get the pair tuple from the hand


let (hasPair, firstValue) = isPair handCopy
// If we have a pair
if hasPair then
// Remove the items that form the pair
// from the hand copy
let handCopy = removePairValue handCopy
(firstValue.Value)

46
match handCopy with
| [] -> (false, None)
| _ ->
// Check if we have a pair once again
let (hasPair, secondValue) = isPair handCopy

// If we do, then we have 2 pairs


if hasPair then (true, Some (firstValue.Value,
secondValue.Value))
else (false, None)
else (false, None)

// Three of a Kind
let _isThreeOfAKind (prevPairValue: Rank) (hand: Card list) =
let handCopy = removePairValue hand prevPairValue

match handCopy with


| [] -> (false, None)
| _ ->
let rec tripleChecker (prevPairValue: Rank) (hand:
Card list) =
match hand with
| h when h.Length = 2 -> (true, Some prevPairValue)
| curr::_ when (curr.Rank |> fst) = prevPairValue
-> (true, Some prevPairValue)
| _::restOfHand ->
match restOfHand with
| [] -> (false, None)
| _ ->
let h = removePairValue restOfHand
(restOfHand.Head.Rank |> fst)
match h with
| [] -> (false, None)
| _ -> tripleChecker (h.Head.Rank |> fst) h
| _ -> (false, None)

tripleChecker prevPairValue handCopy

let isThreeOfAKind = pairOps _isThreeOfAKind

// 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 computeProduct (lst: int list) (i: int) =


lst
|> List.skip i
|> List.take 5
|> List.reduce (fun product prime -> product * prime)

let primeProducts = [for i in [0 .. 8] do yield


computeProduct primes i]

let handPrimeProduct =
hand
|> List.map (fun card -> (card.Rank |> snd))
|> List.reduce (fun product prime -> product * prime)

if handPrimeProduct = 8610 || primeProducts |>


List.contains handPrimeProduct then
(true, hand |> Some)
else (false, None)

// Flush
let isFlush (hand: Card list) =
// We get the first card from the hand
let prev = hand.Head

// Recursive utility function that checks if we have a


flush
let rec _isFlush (_hand: Card list) (iteration: int) =
// If we finished iterating, we'll return true.
// The reason for this is that, if the we don't find a
matching
// suit in the pattern matcher, we return false there
if iteration = hand.Length then (true, Some prev.Suit)

// 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)

// Default case -> suits don't match


| _ -> (false, None)

// We start from the second iteration, since the first


involved getting the first
// card from the hand, which we already did.
_isFlush hand.Tail 1

// Full house
let isFullHouse (hand: Card list) =
let handCopy = hand

let (hasTriple, tripleValue) = isThreeOfAKind handCopy

if hasTriple then
let handCopy = removePairValue handCopy
tripleValue.Value

match handCopy with


| [] -> (false, None)
| _ ->
let (hasPair, pairValue) = isPair handCopy
if hasPair then
(true, Some (pairValue.Value,
tripleValue.Value))
else (false, None)
else (false, None)

// 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)

let isFourOfAKind = pairOps _isFourOfAKind

let isStraightFlush (hand: Card list) =


let (isStraightResult, _) = isStraight hand
let (isFlushResult, _) = isFlush hand
if isStraightResult && isFlushResult then
let suit = hand.Head.Suit
let rankOnlyHand =
hand
|> List.map (fun h -> h.Rank |> fst)
(true, Some (rankOnlyHand, suit))
else (false, None)

let isRoyalFlush (hand: Card list) =


let primeProduct =
hand
|> List.map (fun card -> (card.Rank |> snd))
|> List.reduce (fun prod elem -> prod * elem)

let (isFlushResult, _) = isFlush hand

match (primeProduct, isFlushResult) with


| (31367009, true) ->
let suit = hand.Head.Suit
let rankOnlyHand =
hand
|> List.map (fun h -> h.Rank |> fst)
(true, Some (rankOnlyHand, suit))
| _ -> (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

// The score of any pair should be greater than the


// highest possible value, that being the Ace.
// Thus, we add the score of an Ace to the current
// prime product.
let pairScore = primeProduct + aceValue

// The score of any two pairs should be greater than the


// highest possible pair, that being a pair of Aces.
// Thus, we add the square of the score of Aces to
// the pair rank value
let twoPairScore = pairScore + ([for _ in [0 .. 1] do yield
aceValue]
|> List.reduce (fun prod elem
-> prod * elem))

// The score of any three of a kind should be greater than


// the highest possible two pair, that being a pair of Aces
// and a pair of Kings.
// Thus, we add the square of the score of Kings to
// the two pair rank value
let threeOfAKindScore = twoPairScore + ([for _ in [0 .. 1] do
yield kingValue]
|> List.reduce (fun
prod elem -> prod
* elem))

// The score of any straight should be greater than the


// highest possible three of a kind, that being 3 Aces.
// Thus, we add the cube of the score of Aces to the
// three of a kind rank value
let straightScore = threeOfAKindScore + ([for _ in [0 .. 2]
do yield aceValue]
|> List.reduce
(fun prod elem
-> prod * elem))

// The score of any flush should be greater than the


// highest possible straight (that is not a straight flush /
royal flush).

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)

// The score of any full house should be greater than the


highest
// possible flush (AKQJN of Clubs, Hearts, Diamonds or
Spades).
// Thus, we add the product of the highest possible flush
hand to the
// flush rank value
let fullHouseScore = flushScore + (aceValue * kingValue *
queenValue * jackValue * ninesValue)

// The score of any four of a kind should be greater than the


highest
// possible full house (AAAKK).
// Thus, we add the cube of Ace value multiplied by the
square of King values
// to the full house rank value.
let fourOfAKindScore = fullHouseScore + (([for _ in [0 .. 2]
do yield aceValue]
|> List.reduce
(fun prod elem
-> prod *
elem)) *
([for _ in [0 .. 1] do
yield kingValue]
|> List.reduce
(fun prod elem
-> prod *
elem)))

// The score of any straight flush should be greater than the


// highest possible four of a kind, that being 4 Aces.
// Thus, we add the Ace value to the 4th power to the
// four of a kind rank value.
let straightFlushScore = fourOfAKindScore + ([for _ in [0 ..
3] do yield aceValue]
|> List.reduce

53
(fun prod
elem ->
prod *
elem))

// The score of a royal flush should be greater than the


// highest possible straight flush, that being a King-High
straight (KQJTN).
// Thus, we add the score of the King-high straight hand to
the straight
// flush rank value.
let royalFlushScore = straightFlushScore + (kingValue *
queenValue * jackValue * tensValue * ninesValue)

match ranking with


| HighCard _ -> primeProduct
| Pair _ -> pairScore
| TwoPair _ -> twoPairScore
| ThreeOfAKind _ -> threeOfAKindScore
| Straight _ -> straightScore
| Flush _ -> flushScore
| FullHouse _ -> fullHouseScore
| FourOfAKind _ -> fourOfAKindScore
| StraightFlush _ -> straightFlushScore
| RoyalFlush _ -> royalFlushScore

After implementing the rank evaluation mechanic, I carried on with


implementing 3 mapping functions: one that returns a list of all rank-
ings a card holds, one that returns the highest rank of the hand, based
on the score calculated by the function defined above and one that
computes the scores of the players, which will ultimately be used when
determining who the winner is.
let getRankings (hand: Card list) =
let (isRoyalFlush, royalFlushRank) = isRoyalFlush hand
let (isStraightFlush, straightFlushRank) = isStraightFlush
hand
let (isFourOfAKind, fourOfAKindRank) = isFourOfAKind hand
let (isFullHouse, fullHouseRank) = isFullHouse hand
let (isFlush, flushRank) = isFlush hand
let (isStraight, straightRank) = isStraight hand

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)

let getHighestRank (rankings: HandRanking list) (hand: Card list)


=
rankings
|> List.maxBy (fun _ -> getRankingScore rankings.Head hand)

let computeScores (players: Player list) =


players
|> List.iter (fun player ->
let rankings = getRankings player.Hand
player.Score <- getRankingScore rankings.Head player.Hand
player.HighestRank <- Some <| (getHighestRank rankings
player.Hand))

4.1.11 API - Controller


After defining the engine API, I proceeded to implement the bread and
butter of any back-end for a web app - the controller.

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.

For example, when a user types ”www.google.com” in the address bar


of the browser, the browser sends a GET request to one of Google’s
server’s controller. The controller receives the request and responds
with some data. In this case, it will response to the Google home page.

For big applications, it is good practice to have multiple controllers,


depending on the task at hand. For this particular project, I stuck to
using only 1 controller, since it wasn’t hosting too many routes, nor
was the controller too bloated.

I should also mention that this is not a REST (Representational State


Transfer) API, since it stores data in memory and uses it throughout
the controller’s lifetime. I did this because I didn’t want to add com-
plexity to the application by connecting it to a database.

If this were a big enough of a project, a good idea (which was my


original idea before starting on this project) would’ve been to use a
websockets architecture and a message queue like Apache Kafka, Rab-
bitMQ, etc...
The reason for the websockets architecture is that each user would get
their own unique opponent. Whenever the user dispatches an action, a
message could be published to a topic in the message queue the user’s
CPU is subscribed to. On message received the CPU will respond to
the user and publish a new message to the same topic.
The reason for the message queue is to efficiently keep track of the
state of the game without having to write to a database. Once the
connection from client to server is lost / closed, we can just remove
the topic from the message queue.

56
Regardless, this is how the controller (without the routes) looks
like:
[<ApiController>]
[<Route("[controller]")>]
type PokerController (logger : ILogger<PokerController>) =
inherit ControllerBase()

static let mutable players = []


static let mutable deck = []
static let mutable totalPot = 0

static let getPlayerByName (playerName: string) =


(players
|> List.filter (fun player -> player.Name =
playerName)).Head

static let getCPU (player: Player) =


(players
|> List.filter (fun p -> p <> player)).Head

let getWinnerBasedOnScore (player: Player) (cpu: Player)


(pot: int) =
// Pattern match the players' scores
match (player.Score, cpu.Score) with

// If they have the same score, they split the pot


| (x, y) when x = y ->
let halfPot = pot / 2
player.Chips <- player.Chips + halfPot
cpu.Chips <- cpu.Chips + (pot - halfPot)

player.Bet <- 0
cpu.Bet <- 0

players <- [player; cpu]

let roundOver = true


(players, pot, roundOver)

// If the player's score is greater than the CPU's score


// then the player wins

57
| (x, y) when x > y ->
let (player, cpu) = setWinner player cpu
player.Chips <- player.Chips + pot

players <- [player; cpu]

let roundOver = true


(players, pot, roundOver)

// Otherwise, the CPU wins


| _ ->
let (cpu, player) = setWinner cpu player
cpu.Chips <- cpu.Chips + pot

players <- [player; cpu]

let roundOver = true


(players, pot, roundOver)

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.

The following function is responsible with starting the game:


[<Route("start")>]
[<HttpPost>]
member this.PostStartGame (gameInfo: GameInfo) =
let playerName = gameInfo.playerName
let newGame = gameInfo.newGame

// If this is a new game, reinitialize


// the state of the program
if newGame then
players <- []
deck <- []
totalPot <- 0

let (d, p) = start players playerName


deck <- d

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

// If there are no players, then it must be a new game.


// Create those players
| true ->
[
{
Name = playerName;
Hand = [];
Score = 0;
Chips = 500;
Bet = 0;
WonMatches = 0;
LostMatches = 0;
HighestRank = None;
Challenge = None
};
{
Name = "CPU";
Hand = [];
Score = 0;
Chips = 500;
Bet = 0;
WonMatches = 0;
LostMatches = 0;
HighestRank = None;
Challenge = None
}
]

// If there are players, reset their state


| false ->

59
players
|> List.map (fun p -> {p with Hand = []; Bet = 0;
Challenge = None; Score = 0})

// Deal the cards


let deck = dealCards players deck

// Compute the players' scores based on how strong their hands


computeScores players

// Return the deck and the players


deck, players

In this function we simply create 2 players, deal them cards and com-
pute the scores of their hands.

Additionally, it should be mentioned that this endpoint accepts a


POST request, meaning that the client must send some JSON (JavaScript
Object Notation) data in order for the request to be valid. As such, I
have defined a ”gameInfo” type that maps the shape of the expected
JSON body:
type GameInfo = {
playerName: string
newGame: bool
}

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

let player = getPlayerByName playerName


let cpu = getCPU player

// Discard cards the player chose to discard


deck <- discardCards
player //
cardsToDiscard
false
deck

// Make the CPU randomly discard cards / stand pat


deck <- discardCards
cpu
[]
true
deck

// Compute the players' new scores based on their hands'


strength
computeScores [player; cpu]

players <- [player; cpu]


(deck, players) |> this.Ok

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.

Assuming otherwise can lead to type errors since different program-


ming languages have different definitions for various data types.

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

// Set the player's challenge to Bet, bet the amount and


retrieve the pot
let (player, pot) = player |> getPlayerChallenge
(Some Bet)
amount
0
0
None

// Set the CPU's challenge and retrieve the pot


let (cpu, pot) = cpu |> getCPUChallenge
amount
pot
player.Challenge

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

players <- [player; cpu]

let roundOver = true


(players, pot, roundOver)

// If the CPU's score is the same with the player's and


the CPU called,
// the players will split the pot among themselves.
| cpu when cpu.Score = player.Score && cpu.Challenge =

63
Some Call ->
let halfPot = pot / 2

player.Chips <- player.Chips + halfPot


cpu.Chips <- cpu.Chips + (pot - halfPot)

player.Bet <- 0
cpu.Bet <- 0

players <- [player; cpu]

let roundOver = true


(players, pot, roundOver)

// If the CPU's score is not the same with the player's


and the CPU called,
// the winner is determined based on whose hand is stronger
| cpu when cpu.Score <> player.Score && cpu.Challenge =
Some Call ->
let roundOver = true
match cpu.Score > player.Score with
| true ->
let (cpu, player) = setWinner cpu player
cpu.Chips <- cpu.Chips + pot

players <- [player; cpu]

(players, pot, roundOver)


| false ->
let (player, cpu) = setWinner player cpu
player.Chips <- player.Chips + pot

players <- [player; cpu]

(players, pot, roundOver)

| _ ->
players <- [player; cpu]

let roundOver = false


(players, pot, roundOver)

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

// Set the player's challenge to Raise, raise to the given


amount
// and retrieve the pot
let (player, pot) = player |> getPlayerChallenge
(Some Raise)
amount
cpu.Bet
totalPot
cpu.Challenge

// Set the CPU's challenge and get the amount


let (cpu, pot) = cpu |> getCPUChallenge
amount
pot
player.Challenge

let result =
match cpu.Challenge with

// If the CPU folds, the player wins


| Some Fold ->
let (player, cpu) = setWinner player cpu
player.Chips <- player.Chips + pot

players <- [player; cpu]

let roundOver = true


(players, pot, roundOver)

// If the CPU calls, then we do some pattern


// matching based on the players' scores
| Some Call -> getWinnerBasedOnScore player cpu pot

65
// If the challenge is anything other than a Fold or a
Call, the game continues
| _ ->
let roundOver = false
(players, pot, roundOver)

totalPot <- totalPot + pot


result |> this.Ok

[<Route("game/call/{playerName}")>]
[<HttpGet>]
member this.GetCall (playerName: string) =
let player = getPlayerByName playerName
let cpu = getCPU player

// Set the player's challenge to Call, bet the amount the


opponent
// has bet and retrieve the pot
let (player, pot) = player |> getPlayerChallenge
(Some Call)
cpu.Bet
cpu.Bet
totalPot
cpu.Challenge

let result = getWinnerBasedOnScore player cpu pot

totalPot <- totalPot + pot


result |> this.Ok

[<Route("game/allIn/{playerName}")>]
[<HttpGet>]
member this.GetAllIn (playerName: string) =
let player = getPlayerByName playerName
let cpu = getCPU player

let previousCPUChallenge = cpu.Challenge

let (player, pot) = player |> getPlayerChallenge


(Some AllIn)
player.Chips
cpu.Bet

66
totalPot
cpu.Challenge

let (cpu, pot) = CPURespondToAllIn


cpu
player.Chips
pot

let result =
match previousCPUChallenge with
| Some AllIn -> getWinnerBasedOnScore player cpu pot
| _ ->
match cpu.Challenge with

// If the CPU folds, the player wins


| Some Fold ->
let (player, cpu) = setWinner player cpu
player.Chips <- player.Chips + pot

players <- [player; cpu]

let roundOver = true


(players, pot, roundOver)

// If the CPU calls or also goes AllIn, then we do


some pattern
// matching based on the players' scores
| Some Call
| Some AllIn -> getWinnerBasedOnScore player cpu pot
| _ ->
let roundOver = false
(players, pot, roundOver)

totalPot <- totalPot + pot


result |> this.Ok

[<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

let (cpu, player) = setWinner cpu player


cpu.Chips <- cpu.Chips + pot

let roundOver = true

players <- [player; cpu]

(players, roundOver) |> this.Ok

There is a common formula to all those 5 endpoints:

1. Get the players.

2. Get the players’ challenges.

3. Compute the outcome (result) of the challenge.

4. Add the current pot to the total pot.

5. Respond to front-end with the outcome.

Additionally, all those 5 endpoints make use of 1 common function -


setWinner - whose definition is the following:
let setWinner (winner: Player) (loser: Player) =
winner.Bet <- 0
winner.WonMatches <- winner.WonMatches + 1

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.

In React, everything is a component. A component is a piece of HTML-


like code (called JSX) which is mounted to the DOM (Document Ob-
ject Model). The DOM defines the logical structure of documents and
the way they are used (What is the document object model?, 2021).

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>

As such we could define custom components with our custom props:


interface Props {
myProp: string
}

export function MyComponent({ myProp }: Props) {


return <div>{myProp}</div>
}

If we were to use the ”MyComponent” component like this:


export function App() {
return <MyComponent myProp="Hello CMP5344!" />
}

then it would be equivalent to:


export function App() {
return <div>Hello CMP5344!</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.

Prop drilling involves passing so many props to a component that it


bloats the code, becomes really repetitive and does not allow for clean
code. Take the following component as an example:
interface SomeObject {
someProperty: any
}

interface Props {
prop1: SomeObject,
prop2: any[],
prop3: string,
prop4: string,
prop5: string
}

export function MyBloatedComponent({ prop1, prop2, prop3, ... }:


Props) {
const someVar = prop1.someProperty;

console.log(prop2[0]);

return <div class={prop5}>{prop3}{prop4}</div>


}

70
Now, if we were to use this component in multiple areas, watch how
bloated the code will become:
interface SomeObject {
someProperty: any
}

export function App() {


return (
<div>
<MyBloatedComponent
prop1={{someProperty: "this is a string"}}
prop2={["print this"]}
prop3="Hello, "
prop4="World"
prop5="my-css-class"
/>
<MyBloatedComponent
prop1={{someProperty: "this is another string"}}
prop2={["print that"]}
prop3="Hi "
prop4="there"
prop5="my-other-css-class"
/>
...
</div>
)
}

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.

In this project’s example, the whole front-end is the consumer. The


whole app will make use of the state created by the provider. In turn,
the provider will be in charge of mutating the global state by dispatch-

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;
};

export const initialGameState: GameState = {


deck: [],
playerInfo: null,
CPUInfo: null,
resetGame: false,
players: null,
};

The following is the reducer, which is in charge of mutating the state


given an action:

72
import { Action } from "./actions";
import { GameState } from "./state";

export const gameReducer = (state: GameState, action: Action) => {


switch (action.type) {
case "SET_DECK":
return {
...state,
deck: action.payload,
};
case "SET_PLAYER_INFO":
return {
...state,
playerInfo: action.payload,
};
case "SET_CPU_INFO":
return {
...state,
CPUInfo: action.payload,
};
case "RESET_GAME":
return {
...state,
resetGame: action.payload,
};
case "SET_PLAYERS":
return {
...state,
players: action.payload,
};
default:
return 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'

const GameStateContext = createContext<{


state: GameState,
dispatch: Dispatch<Action>
}>({
state: initialGameState,
dispatch: () => null
})

export const useGameStateContext = () =>


useContext(GameStateContext)

const GameStateProvider: React.FC = ({ children }) => {


const [state, dispatch] = useReducer(gameReducer,
initialGameState)

return (
<GameStateContext.Provider value={{state, dispatch}}>
{children}
</GameStateContext.Provider>
)
}

74
export { GameStateContext, GameStateProvider }

As seen in the snippet, I first created a custom context containing the


state of the app and a dispatch function which is in charge of mutating
the state with the help of the reducer.

I also defined a custom useContext hook that will be useful when fetch-
ing the state and dispatch objects later in the component definitions.

Following, I have defined the provider, which is nothing but a compo-


nent that wraps child components under the context’s provider, which
gives children access to the state and dispatch objects.

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"

import { GameStateProvider } from "./stateManagement/context"


import { Game } from "./components"

function App() {

return (
<GameStateProvider>
<Game />
</GameStateProvider>
)
}

export default App

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
}

export interface GameButton {


text: string,
textColor: string,
bgColor: string,
active: boolean
}

export interface ActionButton {


text: string,
color: string
}

export interface Challenge {


label: string,
disabled: boolean,
onClick: () => void
}

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.

As you will see, most of the functions follow a common pattern:

1. Send request to endpoint

2. Convert response to JSON

3. Do something specific with the received data

In hindsight, perhaps it would’ve been a good idea to define a custom


hook that exactly executes the steps described above, but I figured
code duplication wasn’t that big of a deal for a handful of functions.

If I were to define it, it would’ve probably been something like this:


type Method =
| "GET"
| "POST"
| "PATCH"
| "PUT"
| "DELETE"
| "OPTIONS"

export const makeRequest = (


url: string,
method: Method
callback: (data: any) => void
) => {
fetch(url, { method: method })
.then(response => response.json())
.then(data => callback(data))
}

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"

export const allIn = (


playerName: string,
dispatch: Dispatch<Action>
) => {
fetch(`${apiConfig.api}/poker/game/allIn/${playerName}`)
.then(response => response.json())
.then((data: any) => {
const playerInfo = {
...data.item1[0],
hand: data.item1[0].hand.map(mapPlayerHand),
highestRank:
getHandRanking(data.item1[0].highestRank.value)
}
const CPUInfo = {
...data.item1[1],
hand: data.item1[1].hand.map(mapPlayerHand),
highestRank:
getHandRanking(data.item1[1].highestRank.value)
}

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,
},

// discriminated union types


"item2": {
isType1: false,
isType2: false,
isType3: true,
tag: <index_of_type_that_is_true>
},
...
}

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]

export const mapPlayerHand = (card: any) => {


const rank = (() => {
if(card.rank.item1.isAce) return 1
else if(card.rank.item1.isJack) return 11
else if(card.rank.item1.isQueen) return 12
else if(card.rank.item1.isKing) return 13
else return primes.indexOf(card.rank.item2)
})()

return { rank: rank, suit: card.suit.tag }


}

export const getHandRanking = (ranking: any) => {


const highestRank = Object.entries(ranking).filter((entry) =>
{
return entry[0] !== "tag" && entry[1] === true
})[0][0]
return highestRank.slice(2)
}

80
4.2.4 Setup - Components

In this section, I will show snippets of code defining the components


without showing the objects that style them.

I have used Material UI as my UI framework of choice, so many of


the custom components will use the components defined by the UI
framework (which is also an advocate of inline styling, rather than
standard CSS styling).

I started off by defining a ”Hand” component, which would be in charge


of displaying a hand of cards on the page. The hand can also be hidden
if the opponent is the CPU.

In order to display images of cards on the screen I made use of a


library called react-deck-o-cards.
import { Button } from "@material-ui/core";
import { Dispatch, useEffect, useState } from "react";
import { SetStateAction } from "react";
import { Hand as HandComponent } from "react-deck-o-cards";

import { Card } from "../../interfaces";


import { useGameStateContext } from
"../../stateManagement/context";

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>([]);

const { state } = useGameStateContext();


const { resetGame } = state;

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>
);
}

export default Hand;

This is how a player’s hand of cards looks like:

Figure 1: A player’s hand of cards

This is how a CPU’s hand of cards looks like:

Figure 2: A CPU’s hand of cards

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 { ChallengeModalContent, CustomModal, Hand } from "..";


import { Card, Challenge } from "../../interfaces";

import {
Avatar,
Button,
Card as CardComponent,
CardHeader,
CardContent,
CardActions,
Typography,
CardMedia,
} from "@material-ui/core";

import { red, blue } from "@material-ui/core/colors";


import { SetStateAction } from "react";
import { useGameStateContext } from
"../../stateManagement/context";
import { fold } from "../../services";
import { call } from "../../services/call";
import { allIn } from "../../services/allIn";

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;

const [discarded, setDiscarded] = useState<boolean>(false);


const [betted, setBetted] = useState<boolean>(false);
const [cardsToDiscard, setCardsToDiscard] =
useState<number[]>([]);

const [openBetModal, setOpenBetModal] =


useState<boolean>(false);
const [openRaiseModal, setOpenRaiseModal] =
useState<boolean>(false);

useEffect(() => {
if (resetGame) {
setDiscarded(false);
setBetted(false);
setOpenRaiseModal(false);

86
setOpenBetModal(false);
dispatch({
type: "RESET_GAME",
payload: false,
});
setCardsToDiscard([]);
}
}, [resetGame]);

const playerChallenges: Challenge[] = [


{
label: "Bet",
disabled: isBot || !discarded || betted,
onClick: () => {
setOpenBetModal(true);
},
},
{
label: "Call",
disabled: isBot || !discarded || !betted,
onClick: () => {
call(name, dispatch);
},
},
{
label: "Raise",
disabled:
isBot ||
!discarded ||
!betted ||
(CPUInfo !== null && CPUInfo.bet >= totalChips),
onClick: () => {
setOpenRaiseModal(true);
},
},
{
label: "Fold",
disabled: isBot || !discarded,
onClick: () => {
fold(name, dispatch);
},
},

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>

{challenge === null ? (


<></>
) : (
<CardComponent className={classes.challengeCard}>
<CardContent>
<Typography>

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>
);
}

export default Player;

90
This is how the player components look like:

Figure 3: The players of the Poker game

91
Figure 4: The player’s hand after discarding the first 3 card’s in their
hand

Figure 5: The player betting an amount after clicking ”bet”

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.

The following is the game component, which is in charge of displaying


the players and tying the overall game together.
import { Dispatch, SetStateAction, useEffect, useState } from
"react";
import clsx from "clsx";

import "../../styles/App.css";

import {
Button,
createStyles,
makeStyles,
Theme,
TextField,
Typography,
} from "@material-ui/core";

import { Player } from "..";


import { GameButton } from "../../interfaces";
import { startGame, discardCards } from "../../services";

import { useGameStateContext } from


"../../stateManagement/context";

function Game() {
const { state, dispatch } = useGameStateContext();
const { playerInfo, CPUInfo } = state;

const [playerName, setPlayerName] = useState<string>("");


const [playersStatus, setPlayersStatus] = useState<any>(null);
const [gameStarted, setGameStarted] =
useState<boolean>(false);
const [roundFinished, setRoundFinished] =
useState<boolean>(false);

95
const [gameButton, setGameButton] = useState<GameButton>({
text: "Start New Game",
textColor: "white",
bgColor: startGameColor,
active: true,
});

const resetGameStatus = () => {


dispatch({
type: "RESET_GAME",
payload: true,
});
setPlayersStatus({
playerInfo: { ...playerInfo },
CPUInfo: { ...CPUInfo },
});
};

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:

Figure 8: The initial state of the app

100
Figure 9: Start of Poker game

Figure 10: End of Poker match

101
5 Testing
I chose TickSpec + Expecto as my testing framework of choice.

Expecto is a testing framework that allows us to use Cucumber user


stories as separate tests. This method of testing is called acceptance
testing and it is a testing technique that determines whether the sys-
tem has met the user story specifications or not (TutorialsPoint, 2021).

The way Expecto (and, I presume, most F# acceptance testing frame-


works) works is that, through reflection (which is a metaprogramming
technique used to access the internals of a language), it reads the user
story specifications in the ”.feature” files, parses them and exposes
them to the compiler.

Here’s how the entrypoint file of the testing framework looks like:
module Program

open System.Reflection
open System.IO
open Expecto

let assembly = Assembly.GetExecutingAssembly()


let stepDefinitions = TickSpec.StepDefinitions(assembly)

let featureFromEmbeddedResource (resourceName: string) :


TickSpec.Feature =
use stream = new StreamReader (sprintf "%s" resourceName)
stepDefinitions.GenerateFeature(resourceName, stream)

let testListFromFeature (feature: TickSpec.Feature) :


Expecto.Test =
feature.Scenarios
|> Seq.map (fun scenario -> testCase scenario.Name
scenario.Action.Invoke)
|> Seq.toList
|> testList feature.Name
|> testSequenced

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.

Thanks to this, we can then write tests as follows:


type MyTestSteps() =
let initialState = 0

let myAction state = state + 4

[<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
}

let initializePlayerHand (hand: Card list) (player: Player) =


{ player with Hand = hand }

let populatePlayerHighestRank (player: Player) =


let rankings = getRankings player.Hand
let highestRank = getHighestRank rankings player.Hand
{ player with HighestRank = Some <| highestRank }

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.

If I had more time on my hands, perhaps I would’ve done some things


differently:

1. Rather than having a client interact with the server through


HTTP web requests, I would’ve implemented a web sockets ar-
chitecture

2. I would’ve linked a database to my back-end in order to store


leaderboard details (and perhaps add an ELO matchmaking sys-
tem)

3. I would’ve made use of a message queue to dispatch messages


from client to server and viceversa for scalability

4. I would’ve deployed the app

Nonetheless, I learned a lot regarding back-end development with F#


while working on this project and I can definitely see myself program-
ming while using a declarative style for a living sometime in the future.

111
References
CactusKev. Cactus kev’s poker hand evaluator. Available from: http:
//suffe.cool/poker/evaluator.html.

TutorialsPoint, 2021. Acceptance testing. Available from:


https://www.tutorialspoint.com/software_testing_
dictionary/acceptance_testing.htm.

What is the document object model?, 2021. Available from: https:


//www.w3.org/TR/WD-DOM/introduction.html.

Wikipedia. Fisher–Yates shuffle — Wikipedia, the free en-


cyclopedia. [Online; accessed 22-May-2021]. Available
from: http://en.wikipedia.org/w/index.php?title=Fisher%
E2%80%93Yates%20shuffle&oldid=1019771556.

112

You might also like