PyPortal Blackjack game: uses sprites, touch, audio

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

7 thoughts on “PyPortal Blackjack game: uses sprites, touch, audio”

  1. Pingback: A PyPortal Blackjack game: uses sprites, touch, audio #PyPortal #CircuitPython #Gaming @AndyOfLinux « Adafruit Industries – Makers, hackers, artists, designers and engineers!

  2. Pingback: ICYMI CircuitPython Newsletter: Maintainable code is more important than clever code, FEATHER contest with Digi-Key + Hackaday, and more! #Python #CircuitPython @circuitpython @micropython @ThePSF @Adafruit « Adafruit Industries – Makers, hack

  3. Pingback: Blackjack Game Plays With the Limits of PyPortal

  4. Filed under: CircuitPython, gaming, graphics, Programming, PyPortal Tags: audio, blackjack, game, gaming, pyportal, sprites, touch, touchscreen by Anne Barela Comments Off on A PyPortal Blackjack game: uses sprites, touch, audio #PyPortal #CircuitPython #Gaming @AndyOfLinux

    1. More info needed. What version of CircuitPython are you using? Hmmm. Immediate error or after a while? Did you turn on “DEBUG?” Any more info in the error message? — Thanks, Andy

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top