PyPortal Blackjack Game

I recently visited relatives that frequent casinos at Niagara Falls. I decided to brush-up my Blackjack skills (or lack thereof). I was also looking to enhance my Python (CircuitPython, that is) skills by developing projects, such as this Blackjack game 😉

The Adafruit PyPortal IoT Device with display has everything you need to connect to the internet, build rich GUIs, connect various sensors AND is easy to program in CircuitPython using Adafruit libraries. The “Explore & Learn” area on the Adafruit website is filled with great sample code, nifty apps and lots of inspiration.

When not working on a personal project, I install various projects from Adafruit on my PyPortal. A few of my favorites are the PyPortal Weather Station, the PyPortal ISS Tracker, and the PyPortal NASA Image of the Day Viewer. I also examine and learn techniques from other projects such as the PyPortal Alarm Clock.

After examining the code for these projects and reading documentation for the PyPortal and CircuitPython Libraries, I started developing my Blackjack game. Code examples were great but I had a harder time finding the libraries I needed in order to understand how graphics were displayed. A short guide, “CircuitPython Display Support Using displayio” really helped me understand the use of sprites, graphics as bitmaps, groups, etc. Definitely worth checking out.

Card Element Sprites in a TileGrid

I looked around open-source/free images to use for a Blackjack deck of cards. The size and resolution of the cards would be dependent on the layout of the cards for player and dealer – and needed to fit cards within 320 x 240 pixels. I went for side by side cards with the dealer’s cards on top and the player’s cards on the bottom. I decided that 6 cards across (with no overlap to make things easier) was the maximum needed. Most Blackjack games provide an automatic “win” when a player gets 6 cards with a total under 21. I also needed display space for buttons and messages.

face–down and face-up cards

The use of sprites is ideal for a card game. I used Photoshop to convert a mini SVG playing card set to RGB, then indexed colors, and then saved in a Windows BMP format. These images are distributed under the Creative Commons BY-NC-SA license. The card suits can be represented by 4 sprites (hearts, diamonds, clubs, spades) and the rank cards (2, 3, 4… 10, J, Q, K, A) by 13 X 2 sprites – i.e. 13 ranks in the colors red and black. I also needed a “container” to hold these sprites – i.e. a card outline image. I created a TileGrid with all the various card elements..

My favorite Integrated Development Environment (IDE) for developing Python code on a Mac for the PyPortal is Mu. You can check out some of my other blog posts where are I describe its use.

My PyPortal is currently running Adafruit CircuitPython 4.0.1. I have seen a 5.x alpha release but have not tested under it. I believe if you are running 4.x on your PyPortal, my code should install and run. If you try it under 5.x, let me know!

Mu running on a MacBook Air connected to a PyPortal

The “code.py” file is over 500 lines long. I do NOT recommend this as good practice 😉 I also bounced back-and-forth with classes versus functions. I use global variables (gasp). Developing this game was primarily meant to be a learning process. I wanted to see what the PyPortal could do – and learn how to use sound, graphics, animation(sprites) and fonts. To that end, I was successful.

Quick setup:

Ensure that you PyPortal is setup with the stable version of CircuitPython – version 4.1 at the time of this post.

Download the adafruit-circuitpython-bundle-4.x-mpy-*.zip bundle zip file, and unzip a folder of the same name. Inside you’ll find a lib folder. Add the lib folder to your CIRCUITPY drive. This will ensure you have all the drivers. Note that these files change (are updated) over time – you can find the latest libraries on GitHub.

Notifications with sound

Download and extract files from my PyPortal_Blackjack_1_0.zip zipped folder. MD5 Checksum: e9c28395dd167654be580b76e76cbafd for PyPortal_Blackjack_1_0.zip. Files found in the folder:

card_deck_BMP.bmp – group of card rank and suit sprites

card_image1.bmp – blank card used with sprites for display

code.py – auto-executed python code for game

deal_card_small.wav – card dealt sound effect

dealer_card.bmp – card back image for dealer hidden card

fonts folder – fonts – may need to merge with any existing fonts

poker_chips_small.wav – WIN sound effect

secrets.py – not used by this project – user info – blank!

shut_off.wav – LOSE sound effect

Tracks wins & losses

Assuming you have the CircuitPython libraries on your PyPortal. you can drag all the above files onto your CIRCUITPY drive. You can restart the PyPortal and/or connect to the Mu app and run.

You can edit the “code.py” file to add functionality, fix any bugs or just enhance in general ;-). Note the use of a “DEBUG” flag, set in line 20. When “True,” this prints out interesting info and helped in development and debugging. There are a few thing I have not gotten-to, yet. I do not handle immediate “blackjack”and you cannot split a hand. Please share code and/or comments in the comments section.

code.py

import time import board import displayio import adafruit_imageload import random import adafruit_touchscreen from adafruit_pyportal import PyPortal from adafruit_bitmap_font import bitmap_font from adafruit_display_text.label import Label display = board.DISPLAY # colors GREEN = 0x008800 BLACK = 0x000000 WHITE = 0xFFFFFF GRAY = 0x888888 # Use debug flag to enable display/print of info DEBUG = False pyportal=PyPortal(default_bg=GREEN) button_font = bitmap_font.load_font('/fonts/Arial-16.bdf') button_font.load_glyphs(b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.:/! ') msg_font = bitmap_font.load_font('/fonts/Arial-Bold-24.bdf') msg_font.load_glyphs(b'0123456789abcdefghijklmnopqrstuvwxyzABCDFGHIJKLMNOPQRSTUVWXYZ-,.:/! ') lose_file = '/shut_off.wav' win_file = "/poker_chips_small.wav" deal_card = "/deal_card_small.wav" # define player class for player AND dealer class Player: def __init__(self, name): self.name = name num_cards = 0 num_wins = 0 num_losses = 0 sum_cards = 0 play_card_list = [] player = Player("Player") dealer = Player("Dealer") # take player's hand and center on the bottom of display # some "magic" numbers here as I adjusted values "visually" for a good look def centerPlayerCards(num_cards): card_no = num_cards looper = 0 width = card_no * 50 while looper < card_no: card_x = 160 - int((width / 2)) card_y = 164 spriteCardList[looper].x = card_x + (looper * 50) spriteCardList[looper].y = card_y spriteList[looper].x = card_x + 7 + (looper * 50) spriteList[looper].y = card_y + 4 looper += 1 return True # take dealers's hand and center on top of the display # some "magic" numbers here as I adjusted values "visually" for a good look def centerDealerCards(num_cards): card_no = num_cards looper = 0 width = card_no * 50 card_x = 160 - int((width / 2)) card_y = 5 spriteCardList[12].x = card_x + (looper * 50) spriteCardList[12].y = card_y while looper < card_no: card_x = 160 - int((width / 2)) card_y = 5 spriteCardList[looper+6].x = card_x + (looper * 50) spriteCardList[looper+6].y = card_y spriteList[looper+6].x = card_x + 7 + (looper * 50) spriteList[looper+6].y = card_y + 4 looper += 1 return True # Hit functions add a card to plyer or dealers hand # - assumes there is sprit list of 12 cards where # - first 6 are player's and last 6 are dealer's # - making this sprite list, before game play, enhances performance ;-) def playerHit(): global group player.num_cards += 1 player.play_card_list.append(deck.pop()) centerPlayerCards(player.num_cards) fillCards("player", player.num_cards) # commented out card deal sound as it was erratic in terms of timing # pyportal.play_file(deal_card) group.append(spriteCardList[player.num_cards -1]) group.append(spriteList[player.num_cards - 1]) def dealerHit(): global group dealer.num_cards += 1 dealer.play_card_list.append(deck.pop()) centerDealerCards(dealer.num_cards) fillCards("dealer", dealer.num_cards) # commented out card deal sound as it was erratic in terms of timing # pyportal.play_file(deal_card) group.append(spriteCardList[dealer.num_cards +5]) group.append(spriteList[dealer.num_cards + 5]) # single deck deal - using deck list 52 cards w/ tuples of rank (13), suit (4) def deal(deck): hand = [] for i in range(2): card = deck.pop() hand.append(card) return hand # calculate current hand total - for player or dealer (who) # - handles Ace being 1 or 11 depending on hand total def player_total(who): total = 0 aces = 0 for card in who.play_card_list: if DEBUG: print("Player Card List: " + str(card)) cardval = card[0] if cardval > 9: total+= 10 else: total += cardval if cardval == 1: #cardval of 1 is an Ace - add 1 + 10 to total total+= 10 aces += 1 while aces > 0 and total > 21: # try to avoid a bust by changing ace values from 11 to 1 total -= 10 # until the bust has ben avoided, or you're out of aces aces -= 1 return total # place rank and suite sprites inside card_list # - fast way to update the screen def fillCards(who, num_cards): if who == "player": looper = 0 card_no = num_cards else: # else it is the dealer looper = 6 card_no = num_cards +6 while looper < card_no: if who == "player": player_card = player.play_card_list[looper] else: player_card = dealer.play_card_list[looper - 6] suitL = player_card[1] if suitL < 2: suit_index = suitL else: suit_index = suitL + 13 spriteList[looper][1] = suit_index card_val = player_card[0] if suit_index < 2: spriteList[looper][0] = card_val + 1 else: spriteList[looper][0] = card_val + 16 looper += 1 # create label with player's wins/losses def get_score(): global player score_text = "Wins: " + str(player.num_wins) + " - Losses: " + str(player.num_losses) win_score_text = Label(button_font, text=score_text) win_score_text.x = 160 - int((win_score_text.bounding_box[2])/2) win_score_text.y = 95 win_score_text.color = BLACK return win_score_text # set x, y values and color to center message lable/text def center_label(the_label): label_dims = the_label.bounding_box if DEBUG: print("Bounding box: " + str(label_dims)) the_label.x = 160 - int(label_dims[2]/2) the_label.y = 120 the_label.color = 0x000000 if DEBUG: print('setting up labels...') text_group = displayio.Group(max_size=8) stand_text = Label(msg_font, text=" Stand ") stand_text.x = 10 stand_text.color = BLACK # Make a background color fill dims = stand_text.bounding_box stand_text.y = 140 - int((dims[3] + dims[1]) / 2) if DEBUG: print("STAND bounding box: " + str(dims)) stand_textbg_bitmap = displayio.Bitmap(dims[2]+10, dims[3]+10, 1) stand_textbg_palette = displayio.Palette(1) stand_textbg_palette[0] = 0xfff67b stand_textbg_sprite = displayio.TileGrid(stand_textbg_bitmap, pixel_shader=stand_textbg_palette, x=stand_text.x+dims[0]-1, y=stand_text.y+dims[1]-5) hit_text = Label(msg_font, text=" Hit ") hit_text.x = 232 hit_text.color = 0x000000 # Make a background color fill dims = hit_text.bounding_box hit_text.y = 140 - int((dims[3] + dims[1]) / 2) if DEBUG: print("HIT bounding box: " + str(dims)) hit_textbg_bitmap = displayio.Bitmap(dims[2]+10, dims[3]+10, 1) hit_textbg_palette = displayio.Palette(1) hit_textbg_palette[0] = 0xfff67b hit_textbg_sprite = displayio.TileGrid(stand_textbg_bitmap, pixel_shader=hit_textbg_palette, x=205, y=stand_text.y+dims[1]-5) msg_text = Label(msg_font, text=" BUSTED ") center_label(msg_text) win_text = Label(msg_font, text=" You WIN ") center_label(win_text) push_text = Label(msg_font, text=" PUSH ") center_label(push_text) lose_text = Label(msg_font, text=" You LOSE ") center_label(lose_text) # lose_dims = lose_text.bounding_box # lose_text.x = 160 - int(lose_dims[2]/2) # lose_text.y = 120 # lose_text.color = 0x000000 bj_text = Label(msg_font, text=" BLACKJACK! ") center_label(bj_text) # bj_dims = bj_text.bounding_box # bj_text.x = 160 - int(bj_dims[2]/2) # bj_text.y = 120 # bj_text.color = 0x000000 # Make an enclosing box with background color fill for messages msg_textbg_bitmap = displayio.Bitmap(320, 78, 1) msg_textbg_palette = displayio.Palette(1) msg_textbg_palette[0] = 0xfff67b msg_textbg_sprite = displayio.TileGrid(msg_textbg_bitmap, pixel_shader=msg_textbg_palette, x=0, y= 83 ) # setup touchscreen - values taken from Adafruit example ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR, board.TOUCH_YD, board.TOUCH_YU, calibration=((5200, 59000), (5800, 57000)), size=(320, 240)) # Load the sprite sheet (bitmap) (sprite_sheet, palette) = adafruit_imageload.load('/card_deck_BMP.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette) # Setup 12 cards as this is the max that can be displayed # Create a rank + suit sprite list (tilegrid) spriteList = [] looper = 0 while looper < 12: spriteList.append(displayio.TileGrid( sprite_sheet, pixel_shader=palette, width=1, height=2, tile_width=30, tile_height=30, )) looper += 1 # create the dealer face-down card sprite (dlr_sprite_sheet, dlr_palette) = adafruit_imageload.load('/dealer_card.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette) if DEBUG: print("DEALER Sprite Sheet: ", str(dlr_sprite_sheet)) # create a card outline sprite) (sprite_sheet, palette) = adafruit_imageload.load('/card_image1.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette) # Create a sprite list for card images spriteCardList = [] looper = 0 while looper < 12: spriteCardList.append(displayio.TileGrid( sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=45, tile_height=70, )) looper += 1 # for spriteCardList[12] - add the face-down card spriteCardList.append(displayio.TileGrid( dlr_sprite_sheet, pixel_shader=dlr_palette, width=1, height=1, tile_width=45, tile_height=70, )) # Make the green background color color_bitmap = displayio.Bitmap(320, 240, 1) color_palette = displayio.Palette(1) color_palette[0] = GREEN bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0) # Create a Group to hold the sprites group = displayio.Group(max_size=48, scale=1) group.append(bg_sprite) display.show(group) game = True while game: # get a deck of cards, and randomly shuffle it # deck = get_deck() # print(deck) # random.shuffle(deck) deck = [] while len(deck) < 52: ranks = random.randint(1, 13) suits = random.choice([0, 1, 2, 3]) card = (ranks, suits) if card not in deck: deck.append(card) print('Deck length or shuffled deck: ' + str(len(deck))) # deal 2 cards to player and dealer player.play_card_list = deal(deck) player.sum_cards = player.play_card_list[0][0] + player.play_card_list[1][0] player.num_cards = 2 if DEBUG: print("DeckLength: " + str(len(deck))) print("PlayerHand: " + str(player.play_card_list)) print("PlayerSum: " + str(player.sum_cards)) print("Player Total: " + str(player_total(player))) dealer.play_card_list = deal(deck) dealer.num_cards = 2 if DEBUG: print("DealerHand: " + str(dealer.play_card_list)) print("DeckLength: " + str(len(deck))) score_text = get_score() group.append(score_text) # calculate layout of cards card_num_loop = 3 fillCards("player", 2) time.sleep(.5) fillCards("dealer", 2) time.sleep(.5) looper = 0 face_down_index = 0 while looper < (card_num_loop - 1): centerPlayerCards(looper + 1) group.append(spriteCardList[looper]) group.append(spriteList[looper]) centerDealerCards(looper + 1) group.append(spriteCardList[looper+6]) group.append(spriteList[looper+6]) if looper == 0: group.append(spriteCardList[12]) face_down_index = len(group) - 1 # pyportal.play_file(deal_card) looper += 1 # time.sleep(3) group.append(stand_textbg_sprite) group.append(stand_text) group.append(hit_textbg_sprite) group.append(hit_text) # play the hand quit = False while not quit: player_stand = False busted = False got_touch = False while not player_stand: point = ts.touch_point if point is not None: if DEBUG: print("Point: " + str(point)) if 160 < point[0] < 319: playerHit() got_touch = True p_total = player_total(player) if DEBUG: print("Player Total: " + str(p_total)) if p_total > 21: busted = True player_stand = True quit = True elif 5 < point[0] < 159: player_stand = True got_touch = True quit = True # board.DISPLAY.refresh_soon() # board.DISPLAY.wait_for_frame() time.sleep(0.05) if not busted: d_total = player_total(dealer) group.pop(face_down_index) # if player stands show dealer face-down card while d_total < 17: dealerHit() time.sleep(1) d_total = player_total(dealer) if DEBUG: print("Dealer Total: " + str(d_total)) if d_total > 21: quit = True time.sleep(.5) p_total = player_total(player) d_total = player_total(dealer) if DEBUG: print("Player Total: " + str(p_total)) print("Dealer Total: " + str(d_total)) if p_total > 21: group.pop(face_down_index) # if player is busted, show dealer face-down card if DEBUG: print("BUSTED!") quit = True # If player is under 21 and over dealer or dealer has busted, player wins - update wins/losses if (d_total < p_total < 22) or (d_total > 21): player.num_wins += 1 group.append(msg_textbg_sprite) group.append(win_text) pyportal.play_file(win_file) elif d_total == p_total: # push - if dealer == player group.append(msg_textbg_sprite) group.append(push_text) time.sleep(2) else: player.num_losses += 1 if busted: group.append(msg_textbg_sprite) group.append(msg_text) else: group.append(msg_textbg_sprite) group.append(lose_text) pyportal.play_file(lose_file) # clean up board by pop-ing sprites - nice graphics effect looper = 0 loopend = len(group) - 1 while looper < loopend: group.pop(1) looper += 1 # time.sleep(2) # reinitialize player and dealer for next hand p_wins = player.num_wins p_lost = player.num_losses player = Player("Player") dealer = Player("Dealer") player.num_wins = p_wins player.num_losses = p_lost