Programming: Arduino
Check out my other articles:
- Galaga: Failed Attempt at writing a Game
- Sudoku
- Blockade / Snake
- Farkle
- Minesweeper
- Blackjack
- ADC Graph
Blackjack
My friends at Vizic Technologies challenged me to write a version of Blackjack .. here it is. This is a fully featured version and allows the player to split pairs, double down and take insurance on a dealer blackjack. It is missing a couple of features - such as being able to Surrender a hand (BTW we don't play that in Australia) - but I am guessing another keen developer could easily implement these.
This version uses the 480 x 320 pixel version of the SmartGPU2 but since completing the application I have received a beautiful 480 x 272 SmartGPU2 and I might look at how I can reduce the graphics down to fit on this slightly narrower board.
Overview
This is a classic game of Blackjack. The rules it supports are essentially the same as that found in Las Vegas casinos but the code could easily be changed to support the variations found in other parts of the United States, Europe and Australia.
The rules of this game are as follows:
- This version of Blackjack is played with a single 52-card deck.
- Aces may be counted as 1 or 11 points, 2 to 9 according to pip value, and tens and face cards count as ten points.
- The value of a hand is the sum of the point values of the individual cards. Except, a "Blackjack" is the highest hand - consisting of an ace and any 10-point card, and it outranks all other 21-point hands.
- After the player has bet, the dealer will give two cards to the player and two cards to himself. One of the dealer cards is dealt face up. The facedown card is called the "hole card."
- If the dealer has an ace showing, he will offer a side bet called "insurance." This side wager pays 2 to 1 if the dealer's hole card is any 10-point card. Insurance wagers are optional and may not exceed half the original wager.
- If the dealer has a ten or an ace showing (after offering insurance with an ace showing), then he will peek at his facedown card to see if he has a blackjack. If he does, then he will turn it over immediately.
- If the dealer does have a blackjack, then all wagers (except insurance) will lose, unless the player also has a blackjack, which will result in a push. The dealer will resolve insurance wagers at this time.
- The player now plays his hand. The following are the choices available to the player:
- Stand: Player stands pat with his cards.
- Hit: Player draws another card (and more if he wishes). If this card causes the player's total points to exceed 21 then he loses.
- Double: Player doubles his bet and gets one, and only one, more card.
- Split: If the player has a pair, or any two 10-point cards, then he may double his bet and separate his cards into two individual hands. The dealer will automatically give each card a second card. Then, the player may hit, stand, or double normally. However, when splitting aces, each ace gets only one card.
- After the player has had their turn, the dealer will turn over his hole card. If the dealer has 16 or less, then he will draw another card.
- If the dealer goes over 21 points, then the player wins.
- If the dealer does not bust, then the higher point total between the player and dealer will win.
- Winning wagers pay even money, except a blackjack which pays 3 to 2.
Game Play
The splash screen - like all other graphics in the program - was borrowed from Google. It allows the user to set some basic options before launching the game. These options include whether or not the player can split a pair, whether they can double on split cards and whether a blackjack pays 3 to 2.
If I find time (and motivation) I might add some additional options such as allowing players to only double on a 9, 10 or 11 (known as the Reno rule) or 10 and 11 only (as played in European casinos). Other options might include being able to hit on split aces and allowing the dealer to hit on a soft seventeen. One option - being able to split cards a second time - would require significant rework of the application and a rethink on how these additional hands will be displayed on such a small screen.
Users are able to double or split a pair. When doubling, the dealt card is placed at right angles to the original cards. As the SmartGPU2 library does not support the rotating of images, this 'feature' required a full set of images to be included already rotated. When splitting, the hand currently being played is displayed full size with the other hand rendered in half size - this was done using the SmartGPU2 scaling facility when rendering JPGs.
Insurance is offered when the dealer's first card is an Ace. The player may bet up to one half his original bet as insurance against the dealer having a blackjack. If the dealer has a blackjack he will turn his cards up immediately and take the player's original bet then pay out his insurance. If the player happens to have a blackjack as well the original bet results in a push and the insurance is paid out.
If the dealer's hand does not result in a blackjack, the second card is not turned over and insurances bets are collected by the dealer. Cards that the dealer has seen are shown with a red background to indicate this, as shown in the picture below.
Play concludes when the player either breaks the bank - earns more than $10,000 - or loses all of his money. While playing the game, the number of hands played is recorded along with the number hands won, lost or pushed.
Code Fragments
The following code highlights areas of interest in the application. It is not intended to be a tutorial or a complete solution. I have provided the complete code for download and you may use or modify it as required.
Dealing the CardsFour arrays are used to record the cards dealt from the deck into the dealers hand or either of the two players hands. These are declared in lines 001 - 004.
The cards array declared in line 001 has 52 elements corresponding to the 52 cards in a standard deck. The 0th element corresponds to the Ace of Spades, the 1st element to the 2 of Spades and so forth through to the 12th element which corresponds to the King of Spades. This pattern is repeated for the next 13 elements for the Ace of Clubs through to the King of Clubs and then again for Diamonds and then Hearts. When a card is randomly dealt from the deck, the corresponding element is set to 1 to indicate that it should not be dealt again in the same hand.
The dealer, player_FirstHand and player_SecondHand array holds the cards dealt to each hand. Each element in the array represents a single card with a stored between 0 and 51 representing the cards Ace of Spades through to the King of Hearts (as per the indexing of the card array). Three variables named dealer_CardCount, player_FirstHand_CardCount and player_SecondHand _CardCount are used to keep track of the number of cards dealt for each hand.
001 002 003 004 005 006 007 008 |
byte cards[52]={0}; byte dealer[12]={0}; byte player_FirstHand[12]={0}; byte player_SecondHand[12]={0}; byte dealer_CardCount = 0; byte player_FirstHand_CardCount = 0; byte player_SecondHand_CardCount = 0; |
The getCard() function randomly selects a card represented by a number between 0 and 51 (line 007) and checks to see whether the card has been dealt before in this hand by checking the element in the cards array. If element is zero (indicating that the card has not already been dealt, line 009), the appropriate hand's array is updated with the new card, the card counter increased and the new cards value returned to the calling procedure. If the element is not zero (indicating that the card has already been dealt), a new card is selected and this repeated until a card that has not been previously dealt is found.
The second getCard() function overloads the first and accepts a third parameter that represents the card to be selected (line 044). This function is only used when debugging the program to force a particular situation, such as the dealer having a blackjack or the player getting a pair for splitting.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 |
byte getCard(byte player, byte hand) { byte card = 0; while (true) { card = random(52); if (cards[card] == 0) { cards[card] = 1; break; } } if (player == DEALER) { dealer[dealer_CardCount] = card; dealer_CardCount++; } if (player == PLAYER && hand == FIRST_HAND) { player_FirstHand[player_FirstHand_CardCount] = card; player_FirstHand_CardCount++; } if (player == PLAYER && hand == SECOND_HAND) { player_SecondHand[player_SecondHand_CardCount] = card; player_SecondHand_CardCount++; } return card; } byte getCard(byte player, byte hand, byte cardNumber) { if (player == DEALER) { dealer[dealer_CardCount] = cardNumber; dealer_CardCount++; } if (player == PLAYER && hand == FIRST_HAND) { player_FirstHand[player_FirstHand_CardCount] = cardNumber; player_FirstHand_CardCount++; } if (player == PLAYER && hand == SECOND_HAND) { player_SecondHand[player_SecondHand_CardCount] = cardNumber; player_SecondHand_CardCount++; } return cardNumber; } |
The initialDeal() function shows how the first two cards are dealt to both the player and dealer. Lines 008 - 016 shows the normal operation of the game with randomly generated cards whereas lines 021 - 029 which are commented out can be reinstated to force a particular event - in this case the dealer being dealt a blackjack - for testing and debugging.
The drawCard() function, described in the section Rendering a Single Card below, is used to render a single card at the position supplied in the first two parameters. The third parameter is used to nominate the card to display and - again - is a value from 0 to 51. Note how the dealer's first card (lines 008 - 009) and the player's first card (line010 - 011) are rendered with a random number returned from the getCard() function. The dealer's second card is not displayed to the player initially - line 012 places a call to getCard() generates a random card and places it in the dealers hand but this is not passed to the drawCard() call in lines 013 - 014. Instead a constant CARD_BLUE_BACKGROUND is passed instead which indicates to render the card face down with a blue background. The player's second card is rendered normally at lines 015 - 016.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 |
void initialDeal() { numberOfGamesPlayed++; /* Normal hand */ drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND, CARD_LARGE_TOP_DEALER, getCard(DEALER, FIRST_HAND,0), false, true); drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND, CARD_LARGE_TOP_PLAYER, getCard(PLAYER, FIRST_HAND), false, true); getCard(DEALER, FIRST_HAND); drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND + CARD_LARGE_SPACING, CARD_LARGE_TOP_DEALER, CARD_BLUE_BACKGROUND, false, true); drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND + CARD_LARGE_SPACING, CARD_LARGE_TOP_PLAYER, getCard(PLAYER, FIRST_HAND), false, true); /* Dealer blackjack drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND, CARD_LARGE_TOP_DEALER, getCard(DEALER, FIRST_HAND, 0), false, true); drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND, CARD_LARGE_TOP_PLAYER, getCard(PLAYER, FIRST_HAND), false, true); getCard(DEALER, FIRST_HAND, 10); drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND + CARD_LARGE_SPACING, CARD_LARGE_TOP_DEALER, CARD_BLUE_BACKGROUND, false, true); drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND + CARD_LARGE_SPACING, CARD_LARGE_TOP_PLAYER, getCard(PLAYER, FIRST_HAND), false, true); */ .. } |
Rendering a Single Card
The drawCard() function is used to render a single card at the position supplied in the first two parameters. The third parameter is used to nominate the card to display and - again - is a value from 0 to 51. The final parameters indicate whether the card should be rotated and rendered full size or small. The various combinations are shown below: full size normal orientation, full size rotated, small size normal orientation and small size rotated.
The routine is quite simple really and consists of four switch statements that correspond to the four rendering modes shown above. Each rendering mode is supported by a rendering function - drawCardLarge(), drawCardLarge_Rotated(), drawCardSmall() and drawCardSmall_Rotated() - starting at line number 102.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
void drawCard(int xPos, int yPos, byte card, bool rotated, bool large) { if (large) { if (!rotated) { switch (card) { case 0: drawCardLarge(xPos, yPos, "AS"); break; case 1: drawCardLarge(xPos, yPos, "2S"); break; case 2: drawCardLarge(xPos, yPos, "3S"); break; case 3: drawCardLarge(xPos, yPos, "4S"); break; case 4: drawCardLarge(xPos, yPos, "5S"); break; case 5: drawCardLarge(xPos, yPos, "6S"); break; case 6: drawCardLarge(xPos, yPos, "7S"); break; case 7: drawCardLarge(xPos, yPos, "8S"); break; case 8: drawCardLarge(xPos, yPos, "9S"); break; case 9: drawCardLarge(xPos, yPos, "10S"); break; case 10: drawCardLarge(xPos, yPos, "JS"); break; case 11: drawCardLarge(xPos, yPos, "QS"); break; case 12: drawCardLarge(xPos, yPos, "KS"); break; case 13: drawCardLarge(xPos, yPos, "AC"); break; ... case 26: drawCardLarge(xPos, yPos, "AD"); break; ... case 39: drawCardLarge(xPos, yPos, "AH"); break; ... } } else { switch (card) { case 0: drawCardLarge_Rotated(xPos, yPos, "ASR"); break; ... case 13: drawCardLarge_Rotated(xPos, yPos, "ACR"); break; ... case 26: drawCardLarge_Rotated(xPos, yPos, "ADR"); break; ... case 39: drawCardLarge_Rotated(xPos, yPos, "AHR"); break; ... } } } if (!large) { if (!rotated) { switch (card) { case 0: drawCardSmall(xPos, yPos, "AS"); break; ... case 13: drawCardSmall(xPos, yPos, "AC"); break; ... case 26: drawCardSmall(xPos, yPos, "AD"); break; ... case 39: drawCardSmall(xPos, yPos, "AH"); break; ... } } else { switch (card) { case 0: drawCardSmall_Rotated(xPos, yPos, "ASR"); break; ... case 13: drawCardSmall_Rotated(xPos, yPos, "ACR"); break; ... case 26: drawCardSmall_Rotated(xPos, yPos, "ADR"); break; ... case 39: drawCardSmall_Rotated(xPos, yPos, "AHR"); break; ... } } } } void drawCardLarge(int xPos, int yPos, char card[2]) { lcd.drawRoundRect(xPos, yPos, xPos + CARD_LARGE_WIDTH, yPos + CARD_LARGE_HEIGHT, radius5, WHITE, solidFill); lcd.drawRoundRect(xPos, yPos, xPos + CARD_LARGE_WIDTH, yPos + CARD_LARGE_HEIGHT, radius5, BLACK, hollowFill); lcd.imageJPGSD(xPos + CARD_LARGE_INSET, yPos + CARD_LARGE_INSET, SCALE1_1, card); } void drawCardLarge_Rotated(int xPos, int yPos, char card[2]) { lcd.drawRoundRect(xPos, yPos + CARD_LARGE_ROTATED_Y_OFFSET, xPos + CARD_LARGE_HEIGHT, yPos + CARD_LARGE_ROTATED_Y_OFFSET + CARD_LARGE_WIDTH, radius5, WHITE, solidFill); lcd.drawRoundRect(xPos, yPos + CARD_LARGE_ROTATED_Y_OFFSET, xPos + CARD_LARGE_HEIGHT, yPos + CARD_LARGE_ROTATED_Y_OFFSET + CARD_LARGE_WIDTH, radius5, BLACK, hollowFill); lcd.imageJPGSD(xPos + CARD_LARGE_INSET, yPos + CARD_LARGE_ROTATED_Y_OFFSET + CARD_LARGE_INSET, SCALE1_1, card); } void drawCardSmall(int xPos, int yPos, char card[2]) { lcd.drawRoundRect(xPos, yPos, xPos + CARD_SMALL_WIDTH, yPos + CARD_SMALL_HEIGHT, radius5, WHITE, solidFill); lcd.drawRoundRect(xPos, yPos, xPos + CARD_SMALL_WIDTH, yPos + CARD_SMALL_HEIGHT, radius5, BLACK, hollowFill); lcd.imageJPGSD(xPos + CARD_SMALL_INSET, yPos + CARD_SMALL_INSET, SCALE1_2, card); } void drawCardSmall_Rotated(int xPos, int yPos, char card[2]) { lcd.drawRoundRect(xPos, yPos + CARD_SMALL_ROTATED_Y_OFFSET, xPos + CARD_SMALL_HEIGHT, yPos + CARD_SMALL_ROTATED_Y_OFFSET + CARD_SMALL_WIDTH, radius5, WHITE, solidFill); lcd.drawRoundRect(xPos, yPos + CARD_SMALL_ROTATED_Y_OFFSET, xPos + CARD_SMALL_HEIGHT, yPos + CARD_SMALL_ROTATED_Y_OFFSET + CARD_SMALL_WIDTH, radius5, BLACK, hollowFill); lcd.imageJPGSD(xPos + CARD_SMALL_INSET, yPos + CARD_SMALL_ROTATED_Y_OFFSET + CARD_SMALL_INSET, SCALE1_2, card); } |
Rendering a Full Hand
By calling the drawCard() function for each element of the dealer, player_FirstHand or player_SecondHand arrays we can draw the complete hand. Below are two functions - drawFirstHand_Large() and drawFirstHand_Small - that draw the player's first hand in large and small formats respectively. Not shown are the two functions that perform the same functions for the player's second hand if he has split a pair.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 |
void drawFirstHand_Large() { for (int x=0; x< player_FirstHand_CardCount; x++) { if (x == player_FirstHand_CardCount - 1 && player_FirstHand_Double) { drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND + (x * CARD_LARGE_SPACING), CARD_LARGE_TOP_PLAYER, player_FirstHand[x], true, true); } else { drawCard(CARD_LARGE_LEFT_PLAYER_FIRSTHAND + (x * CARD_LARGE_SPACING), CARD_LARGE_TOP_PLAYER, player_FirstHand[x], false, true); } } } void drawFirstHand_Small() { for (int x=0; x< player_FirstHand_CardCount; x++) { if (x == player_FirstHand_CardCount - 1 && player_FirstHand_Double) { drawCard(CARD_SMALL_LEFT_PLAYER_FIRSTHAND + (x * CARD_SMALL_SPACING), CARD_SMALL_TOP_PLAYER, player_FirstHand[x], true, false); } else { drawCard(CARD_SMALL_LEFT_PLAYER_FIRSTHAND + (x * CARD_SMALL_SPACING), CARD_SMALL_TOP_PLAYER, player_FirstHand[x], false, false); } } } |
Wiring Diagram
The diagram below show the wiring diagram when using a standard Arduino Uno (or compatible) and the SmartGPU2. Click on the image to see a detailed drawing.
Downloads
Feel free to download, modify and adapt this code to your needs. Please credit me if you use some or all of this code in your own project. If you alter the code to fix bugs or add functionality, please submit them back to me and I will add them to the download.
Arduino Source Code |