*Or any SBC supported in Maker.MakeCodePart 1 (of 4): Creating a virtual pack of cards.
There are seven sections in Part 1 of the project::
- Creating an array to represent a pack of cards
- Shuffling the virtual pack
- Dealing a card / hand
- Interpreting the cards
- Next steps
Building genuinely playable games on micro:bit is tricky!
The microprocessor is too limited for anything that requires fast frame-by-frame progression. In addition, you either use the micro:bit 5x5 LED matrix as a display, or else you need an add-on, which cuts into your microprocessor resources. Even with the excellent MakeCode Arcade, these limitations mean micro:bit just isn't a viable gaming platform.
I've tried to build a few myself... my Tetris was terrible, Pong stunk and Simon - well, that wasn't too bad actually. But not great.
But card games are not necessarily affected by these limitations, and there are many that are simple and compelling, I set out to build a 1-player card game with a single primary objective:
The game had to be good enough that if I was stuck waiting for bus or a train with nothing else to do then it would be more fun to play the game than to do nothing.
A high bar - I hope you will agree that this Blackjack project doesn't limbo too far below it!
I've split the project into 4 parts, and these are detailed at the end. Each part will be a separate Hackster project, with Part 2 building on Part 1, and Part 3 building on Part 2 etc.
Here we look at Part 1: Creating a virtual deck of cards.
- In this part we look at how to emulate a pack of cards in code, including how to shuffle it and deal a hand.
- This part is totally reusable for any card game that you might choose to build.
----------------------------------2: Creating an array to represent a pack of cards:
A pack of cards is, quite conveniently, the perfect analogy for an array. Imagine you have a pack of cards in your hands:
- You are holding 52 pieces of unique information - 52 cards, each with a unique suit/face-value pair.
- Each card has a position in the deck. So, there is a specific value at each location in the deck. The card at position 24 might be the nine of hearts.
- The pack of cards is a real-world 52-element array. We will create a 52 element array in code to emulate this.
- In the real world each physical card is encoded with a visual number and suit. In code we will store a number between 0 and 51 in each location in the array.
- That number, between 0 and 51, represents a suit/face-value pair.
To represent a pack of cards in code we will therefore create a 52-element* array. We will refer to this 'virtual pack' in code as our deckOfCards:
* But what about the 2 jokers, you might wonder: note in the code that I have allowed the dimensions of the pack to be specified (by using the variable packSize to define the size of the array deckOfCards - I could have used the constant 52, but this allows me to be flexible).
You COULD create a 54 card array, and assign the extra 2 cards as jokers. You could probably even create a 104 card array and play with 2 packs - at some point limitations in the micro:bit memory will come into play though.
Our virtual pack - deckOfCards - is ready, but right now it effectively contains 52 blank cards. We will write an integer between 0 and 51 onto each card, then the deck will be ready to play with. We do this in the next section...3: Shuffling the virtual pack:
In the real world, when you shuffle a pack of cards:
- You randomly change the order of the cards in a pack.
- The shuffled pack is populated by a predictable set of cards - we know what each of the 52 cards are (there is never going to be a 16 of spades). We just don't know where they are the deck.
- When you finish shuffling each card has taken a random place in the deck / array. That order is then 'fixed' until all the cards are dealt / used, at which point the pack is shuffled again. You don't keep shuffling an already shuffled pack.
When we shuffle in code what we do is similar: we populate the deckOfCards array we created above by adding a random and UNIQUE suit/face-value (an integer from 0 to 51) to each location in the array.
To support this we add a few bits to the On Start block:
- we set a variable called numberPackShuffles. We are going to use this to count the number of times the pack is shuffled in a game. It feels a bit spurious now, but it comes into play later and in testing we will use it to verify the pack is shuffled automatically when the last card has been dealt..
- we call a function shufflePack() when we need to shuffle. The heavy lifting is done in that function.
The main challenge we face in the shufflePack() function is the requirement that each card we place into deckOfCards is unique, and also valid: Every integer from 0 to 51 MUST be represented in our virtual pack, but only once, and in a random location..
When you shuffle a pack in real life you don't expect cards to spontaneously clone or cease to exist. You start with 52 cards - four suits, aces through kings - and you end with the same cards. Shuffling will not result in a fourteen-of-swords card blooming into existence, and only stage magicians can turn every card into the ace of spades.
The shufflePack() function will implement the following algorithm:
- Imagine taking a pack of cards that are sorted in numerical and suit order. So the first card is the ace of hearts, then the 2 of hearts, then the 3... and the last card is the king of spades. Like a new deck when you buy it and use it the first time. Imagine each card is numbered from 0 to 51 sequentially.
- Call this pack the Origin Pack. The Origin Pack is represented by a 52 element array (which we call originPack in code).
- This array contains the numbers 0 to 51 in order. So element 0 = 0, element 1 = 1, element 23 = 23 etc. This means that originPack is effectively a deck of cards that is sorted, like a brand new deck.
- Shuffling will be implemented by removing cards at random, one by one, from originPack and placing them in a separate pack: deckOfCards.
This algorithm is equivalent to shuffling a deck of cards in the real world like this:
- Pick a random number between 0 and 51.
- Take the card from that location in the originPack array and place it into a table (this will become deckOfCards).
- Your Origin Pack now has 51 cards, still in order. deckOfCards has just 1 card,
- Pick a random number between 0 and 50. NOTE - there is 1 card less in originPack so 1 less location to choose randomly from.
- Take the card from that location in originPack and place it onto deckOfCards - place it under the card that is already there.
- OriginPack now has 50 cards, still in order. deckOfCards has 2 cards in no order.
- Pick a random number between 0 and 49... etc etc.
This algorithm is not how a person shuffles a deck of cards in the real world, but it would work - its a viable, albeit cumbersome, method. But its a much easier algorithm to implement in code then trying to emulate some of the fancy shuffling techniques you see on TV!
We will look at the function shufflePack() in a moment. As soon as it is called it calls the resetPack() function, so lets check that first:
What we have done above is ensure that the originPack array is the right length, and each element of the array maps to a unique playing card. It is also sorted - so the ace of hearts is at the top, below it the 2 of hearts, then the 3, etc. We do this by putting an integer value from 0 to 51 into the array.
We are now ready to implement the shuffling algorithm described earlier, which is done in the shufflePack() function:
In the code above, once we have reset the originPack we enter a loop, 52 iterations long. In each iteration we remove a card from a random location in originPack and we add it to deckOfCards. By the end of the process all the cards have been removed from originPack - the array has zero elements. All 52 locations in the deckOfCards array now contain a unique number from 0 to 51.
The virtual deck - deckOfCards - is now ready to play with.
Note in the code above that we set a counter activeCard_PackPosition to 0. This is a counter that is KEY to using our virtual deck of cards:
- This variable keeps track of the current card - the next one to be dealt.
- When we deal a card we will take a card from position activeCard_PackPosition in deckOfCards, then we will increment the activeCard_PackPosition counter by 1..
- We DON'T change the deckOfCards array at all - unlike in the real world, we don't discard cards as we deal them. In effect we have a FOCAL card - the currently active card - the one in activeCard_PackPosition.
- When we reach activeCard_PackPosition = 52 we have dealt all the cards in deckOfCards and its time to shuffle again.
Our virtual deck of cards, deckOfCards, is now shuffled and ready to play with.
Regardless of the game we decide to encode, it is going to be necessary to deal a number of cards to a player - to deal them a hand (in the parlance of card games a hand is a set of cards that a player holds).
In code we will need to take a number of cards from deckOfCards and put them into a hand.
In this project the objective is to be as generic as possible... to build a code base on which any number of cards games can be designed. But the process of dealing is often unique to the game you are playing: consider a game of blackjack, compared to a game of poker, or solitaire. In later iterations of this project, when we build blackjack, we will customise the deal function to the specific requirements of the game.
For this, generic implementation we will assume the game has only 1 player, as in solitaire. In addition we will assume that a hand involves up to 5 different cards. These parameters are easy enough to change: have more or less cards in a hand and add more players (for blackjack we need 2 players).
We need a way to take the 'top' card (in position activeCard_PackPosition) from deckOfCards and add it to an array we will caller playerHand.
Firstly, the On Start block needs a bit more stuff - we need to initialise playerHand, and we'll keep a count of how many hands are played... its useful later on and in testing.:
In later iterations of this project we will look at how the startNewHand() function is adapted for different game types. For now we will keep it as simple as possible: we will deal 5 cards to the player then stop.
Some notes on the code above:
- It is tempting to reset the playerHand array before dealing a new hand. But this is unnecessary: the counter playerHandSize tells us how many elements of the playerHand array to consider as being the 'current' hand. Resetting playerHandSize to zero at the beginning of each hand is adequate.
- The code that we use to deal a card - the getDealtCard() function - could be used to populate any array, not just the playerHand array. This means we could use the same code to deal a card into any hand (for games where we might have more than 1 player).
- Note also that we increment activeCard_PackPosition here, rather than in the getDealtCard() function. This is NOT an insignificant design decision at all... it amounts to recognising that dealing a card does not necessitate discarding the card (more details on this below)
Lets take a look at the getDealtCard() function (and note that it returns an integer):
This function is short and simple, but there is quite a bit going on:
- This function ensures the pack is shuffled when the last card has been dealt.
- The integer value of the card in activeCard_PackPosition is returned.
- THE CARD REMAINS 'ACTIVE' (until we increment activeCard_PackPosition).
- The card is not dealt to any specific player in this function. We assigned it to the player hand...not in this function butin startNewHand().
- This remains the active card until we increment activeCard_PackPosition. That does NOT happen in this function.
- So, when we deal a card in this function we don't deal it to a specific player: we don't decide how to use it at all. And we don't 'discard' it. All that is done outside of getDealtCard().
One consequence of this approach is that the same card could be dealt to several different players simultaneously. Whilst this may sound counter-intuitive, consider Texas Hold-um and Snap - perhaps similar only in that the game dynamic includes 'shared' cards - ones that are simultaneously in several players' hands.
So, this approach is flexible, but we MUST make sure we remember to increment activeCard_PackPosition when necessary. We have broken down the activities of a real-world dealer into 2 parts:
- calling out the currently active card,..
- then retiring / discarding that card.
We must make sure we remember to undertake both parts when dealing. The draw-back to not taking care of this in getDealtCard() is that we need to do it manually / carefully in code. In Part 3 we will bury it in a dealCard() function that does everything for us, but for now we'll have to increment it ourselves.
We're pretty much done now - the code we have developed will create a pack of cards, shuffle it, then deal out 5 to a player. But these cards are still just integers between 0 and 51. We are going to need to be able to convert these integers into values that are more meaningful in the context of a game of cards. In the next section we'll look at interpreting these integer values...5: Interpreting the cards
Cards are stored in deckOfCards as integers between 0 and 51. We need a convenient way of converting these numbers into values that are of interest to our game of cards. We need functions that we can pass in a value between 0 and 51 and which will return a variety of values... the table below shows a number of different values which we might need to interpret from the integer (referred to as ID in the table below)::
The game you are developing will determine which values you need to convert the integer representing the card into. For example, in blackjack the suit is not relevant to the game mechanic, just the value of the card... but we need to know the high and low values. In poker the suit is very relevant, as is the card type, but the value itself not so much. It is not clear which values from the lookup table above we will need for our game, so we'll focus on testing.
We'll start by building a function to return the value referred to in the lookup table above as Int ID. This is a number from 0-12 that tells us which type of card it is (Ace = 0, King = 12, Queen = 11 etc). If we extract Int ID from ID then we can convert it into FACE (J, Q, K, A, 2, 3 etc)... a regonisable way of identifying a card.
The getCardFaceID() function is simple (it is only 1 line - you could argue you don't need it) but its a ton easier later on to just use the function name in your code. We infer the Int ID using 'modular' division:
A value from 0 to 12 is returned, which uniquely identifies the face of the card: we have calculated Int ID from the lookup table above.
Next we'll infer suit. We use the Int ID (which we will call the card face) to calculate the suit (as an integer from 0-3) using straightforward division:
Note - this function may be more complicated that it needs to be - what we've done is remove the remainder before dividing to avoid the code interpreter rounding integers - something we can't control. You might be able to simplify this though - a reliable RoundDown() function that you trust will be effective.
The final function we will build below is stepping on the toes of Part 2 just a bit, but it will make the testing experience a lot easier and more complete. What I want to do in testing is the following:
- Create and shuffle a pack of cards
- Deal a hand of 5 cards
- Go through the cards in the hand 1-by-1 - firstly seeing the int (0-51) then seeing that interpreted into a playing card
- Deal more hands, confirm no duplicates
- Confirm pack is reshuffled after 52 cards are dealt
The final function (which is actually 3 functions, only 1 of which we call directly) converts an integer (0-51) into a 2 character string that represents the playing card, so: 0 = "AH", 1 = "2H", 16 = "4S", 49 = "JC" etc. The full code (all 3 functions is listed below):
- All we are doing above is converting integers into 2 character long strings based on the lookup table. The strings are recognisable as playing cards.
- We use "T" for 10 - just to keep the strings to 2 characters in length.
We are going to build a very simple test rig here - the limitations of the visual output make extensive testing very onerous and time consuming. The tools we will use in Part 2: Displaying Cards on a Screen will make it easy to test more extensively.
For our test we will verify that hands are being dealt correctly, that cards are being interpreted correctly, and that the pack is shuffled when is supposed to be. We'll write some code that does the following:
- When the program loads a hand (of 5 cards) is dealt.
- When a user clicks the A-button we will output the content of their hand onto the micro:bit 5x5 LED matrix. Firstly the ID (0-51) representing the card is shown, then "=", then the 2-char string representation of the card, followed by a dot. All 5 cards in the hand scroll, after which a tick is shown.
Verify that there are 5 cards in a hand, that all of them have an ID between 0 and 51, that each ID maps correctly to a card (as per the lookup table) and there are no duplicates UNTIL the pack is shuffled.
- What a user clicks the B-button a new hand is dealt. The total number of hands dealt is listed, followed by an X, then the number of shuffles will be printed, followed by a tick.
Watch the number of hands / shuffles to verify that the pack was shuffled when it was supposed to be and ensure that there are no duplicate cards until the pack is shuffled.
I've split the project into 4 parts, for 2 reasons:
- it is long. With all 4 parts together, very long.
- taking the first 3 parts together shows you how to build a game of blackjack using MakeCode block coding... you can use it on micro:bit or any of the SBCs supported by Maker.MakeCode.
- Part 1 and Part 2 are reusable - the foundation for any card game. So you can add your own Part 3 :)
The 4 parts are listed below:
Part 1: Creating a virtual deck of cards.
- In this part we looked at how to emulate a pack of cards in code, including how to shuffle it and deal.
- The manner in which you display cards in your games will be determined by the output options available to you... I use an SSD1306 OLED display (128x64 pixels) - the OD01 from XinaBox. The architecture implemented here - all the functions and function calls - will work whatever display you use. Just keep the same structure and switch out the lines of code that are specific to your display mode. When you have a method that works for you it will be reusable across your card games.
- Twist or stick, blackjack or bust, aces high or low? Blackjack is a relatively simple game, but there are a few quirks.
The idea of this project is to build Part 1 and Part 2 in such a way that any card game could be built using the code in these parts. In Part 3 we build Blackjack on top of this foundation, but you could go for poker, solitaire, snap - anything.
Part 4: Building a sim component
In Part 3 we set up some parameters, such as what score the dealer sticks on, and what amount the player bets based on their first card.
Imagine setting the dealer stick-limit to 15, running 1000 hands, then changing to 16. What difference would it make - would you win more or less? And is it better to bet high on Aces and picture cards and low on the other cards - is there a betting strategy that maximises your winnings (or minimises your losses)?
We'll build a sim element so you don't even have to play the game yourself! Run it for (say) 1000 hands, store the data, then analyse it. Great for learning and trying out different strategies. Perhaps you can find a way to beat the bank?
BonusPart 5: Multiplayer. Why not? I've not written it yet though
Right now I have a decent, working version of Blackjack, which is what I am writing up... I don't have a multi-player version yet though, so we'll call this dream-ware, for now.
Thanks for reading and please check out Part 2 :)
This project is meant to be easy to read and relatively simple to implement. It shows you how to achieve a useful outcome without labouring on irrelevant technical details. If you like this style please check out my book: Beginning Data Science, IoT and AI Using Single Board Computers.