5
\$\begingroup\$

This is the latest version of my Blackjack game that uses a MySQL database to store user info. I did everything I was recommended in my previous post, added a ranking system and also fixed a bug where you could make infinite money.

from random import shuffle
import os
import cymysql
from getpass import getpass
import sys
import re
from bcrypt import hashpw, gensalt


def shuffled_shoe():
    shoe = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'A', 'J', 'Q', 'K']*4
    shuffle(shoe)
    return shoe


def deal_card(shoe, person, number):
    for _ in range(number):
        person.append(shoe.pop())


def deal_hand(shoe, player, dealer):
    for _ in range(2):
        deal_card(shoe, player, 1)
        deal_card(shoe, dealer, 1)


def score(person):
    non_aces = (c for c in person if c != 'A')
    aces = (c for c in person if c == 'A')
    total = 0
    for card in non_aces:
        if card in 'JQK':
            total += 10
        else:
            total += int(card)
    for card in aces:
        if total <= 10:
            total += 11
        else:
            total += 1
    return total


def set_money(money, money_bet, win, push):
    if win:
        money += money_bet * 2
    elif push:
        money += money_bet
    return money


def clear_console():
    os.system('cls' if os.name == 'nt' else 'clear')


def display_info(still_playing, player, dealer, money, money_bet, player_stands):
    win = False
    push = False
    clear_console()
    print(f'Money: ${money}')
    print(f'Money bet: ${money_bet}')
    print('Your cards:   [{}] ({})'.format(']['.join(player), score(player)))
    if player_stands:
        print('Dealer cards: [{}] ({})'.format(']['.join(dealer), score(dealer)))
    else:
        print('Dealer cards: [{}][?]'.format(dealer[0]))
    first_hand = len(dealer) == 2
    if score(player) == 21:
        print('Blackjack! You won')
        still_playing = False
        win = True
    elif first_hand and score(dealer) == 21:
        print('Dealer got a blackjack. You lost!')
        still_playing = False
    elif score(player) > 21:
        print('Busted! You lost!')
        still_playing = False
    if player_stands:
        if score(dealer) > 21:
            print('Dealer busted! You won')
            win = True
        elif score(player) > score(dealer):
            print('You beat the dealer! You won!')
            win = True
        elif score(player) < score(dealer):
            print('Dealer has beaten you. You lost!')
        else:
            print('Push. Nobody wins or losses.')
            push = True
        still_playing = False
    money = set_money(money, money_bet, win, push)
    return still_playing, money


def hit_or_stand():
    while True:
        print('What do you choose?')
        print('[1] - Hit')
        print('[2] - Stand')
        ans = input('> ')
        if ans in '12':
            return ans


def bet(money):
    clear_console()
    print(f'Money: ${money}')
    print('How much money do you want to bet?')
    while True:
        money_bet = int(input('> '))
        if money_bet <= money and not money_bet <= 0:
            money -= money_bet
            return money, money_bet
        print('Please enter a valid bet.')


def player_play(shoe, player, dealer, money, money_bet, player_plays, player_stands):
    while not player_stands:
        if hit_or_stand() == '2':
            player_stands = True
            player_plays = False
        elif not player_stands:
            deal_card(shoe, player, 1)
            display_info(True, player, dealer, money, money_bet, player_stands)
            if score(player) >= 21:
                player_plays = False
                break
    return player_plays, player_stands


def dealer_play(shoe, dealer, dealer_minimum_score):
    while score(dealer) <= dealer_minimum_score:
        deal_card(shoe, dealer, 1)
    return False


def check_money(money):
    if money == 0:
        print('\nUnfortunately you do not have any money.')
        sys.exit()


def update_db_money(cur, money, email):
    cur.execute('UPDATE `users` SET `money`=%s WHERE `email`=%s', (money, email))


def play_again(money):
    check_money(money)
    while True:
        print('\nDo you want to play again? [Y]es/[N]o')
        ans = input('> ').lower()
        if ans == 'y':
            return True
        elif ans == 'n':
            return False


def get_user_info():
    while True:
        email = input('Email address (max. 255 chars.): ')
        password = getpass('Password (max. 255 chars.): ').encode('utf-8')
        hashed_pw = hashpw(password, gensalt())
        if len(email) < 255 and len(password) < 255:
            if re.match(r'[^@]+@[^@]+\.[^@]+', email):
                return email, password, hashed_pw
            print('Please enter a valid email address.')


def register(cur, email, hashed_pw):
    cur.execute('INSERT INTO `users` (`Email`, `Password`) VALUES (%s, %s)', (email, hashed_pw))


def login(cur, email, password, hashed_pw):
    cur.execute('SELECT * FROM `users` WHERE `Email`=%s LIMIT 1', (email,))
    correct_credentials = cur.fetchone()
    correct_hash = correct_credentials[2].encode('utf-8')
    if hashpw(password, correct_hash) == correct_hash:
        print('You have succesfully logged-in!')
    else:
        print('You failed logging-in!')
        sys.exit()


def check_account(cur, email):
    cur.execute('SELECT * FROM `users` WHERE `Email`=%s LIMIT 1', (email,))
    return bool(cur.fetchone())


def display_top(cur):
    cur.execute('SELECT * FROM `users` ORDER BY `money` DESC')
    top = cur.fetchall()
    places = range(1, len(top)+1)
    for (a, b, c, d), i in zip(top, places):
        print(f'{i}. {b} - ${d}')


def start():
    print('\nWhat do you want to do?\n1 - Start playing\n2 - Display the top')
    ans = input('> ')
    if ans == '1':
        return True
    elif ans == '2':
        return False


def db_conn():
    conn = cymysql.connect(
        host='127.0.0.1',
        user='root',
        passwd='',
        db='database'
    )
    with conn:
        cur = conn.cursor()
        email, password, hashed_pw = get_user_info()
        checked = check_account(cur, email)
        if checked:
            login(cur, email, password, hashed_pw)
        else:
            register(cur, email, hashed_pw)
            print('You have succesfully registered and recieved $1000 as a gift!')
        cur.execute('SELECT `money` FROM `users` WHERE `email`=%s', (email,))
        money_tuple = cur.fetchone()
        money = money_tuple[0]
        check_money(money)
        return cur, money, email


def main():
    cur, money, email = db_conn()
    keeps_playing = start()
    if not keeps_playing:
        display_top(cur)
    while keeps_playing:
        shoe = shuffled_shoe()
        player = []
        dealer = []
        still_playing = True
        player_plays = True
        player_stands = False
        money, money_bet = bet(money)
        deal_hand(shoe, player, dealer)
        still_playing, money = display_info(still_playing, player, dealer, money, money_bet, player_stands)
        while still_playing:
            while player_plays:
                player_plays, player_stands = player_play(shoe, player, dealer, money, money_bet, player_plays, player_stands)
            still_playing = dealer_play(shoe, dealer, 17)
        still_playing, money = display_info(still_playing, player, dealer, money, money_bet, player_stands)
        update_db_money(cur, money, email)
        keeps_playing = play_again(money)
    cur.close()


if __name__ == '__main__':
    main()

Database:

SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `email` varchar(255) COLLATE utf8_bin NOT NULL,
  `password` varchar(255) COLLATE utf8_bin NOT NULL,
  `money` int(11) NOT NULL DEFAULT '1000',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
\$\endgroup\$
1
\$\begingroup\$

This is a prime candidate for object orientation - you clearly have concepts like user, game, hand, shoe, dealer, etc. Each of these would keep track of their respective pieces of data — the User class would have a money field, for example.


A method called display_info would not be expected to return anything - it should simply receive some information and display it.


I always suggest users to run their code through Black, flake8 and mypy with a setup.cfg something like this:

[flake8]
doctests = true
exclude =
    .git,
    __pycache__
max-complexity = 4
max-line-length = 120
ignore = W503,E203

[mypy]
check_untyped_defs = true
disallow_untyped_defs = true
ignore_missing_imports = true
no_implicit_optional = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_ignores = true

This will help making your code simple, readable and explicit (but will never guarantee it). Understanding and acting on all the information you'll get from these tools is super helpful to write idiomatic and clear Python code.


This is an excellent example of code which could be test driven. TDD is probably the hardest thing I've ever learned, but it's an incredibly powerful way to achieve confidence in your code and to be sure that you can act on any future change requests without fear of breaking existing functionality (because if you do, your tests will catch it).


Database interaction code should not be in the main method - typically that should only create the main object and set it going. Your connection parameters should be arguments or configuration.


You never call deal_card with any number other than 1. YAGNI - just remove that parameter and deal one card.


Single letter variables are terrible for readability. c is score should simply be card, for example.


Rather than string matching on the various cards to get the score it looks like cards should be objects with a score field (or method if the score depends on external state). That way you could simply return sum([card.score for card in cards]).


You should update the database WHERE id = %s rather than keying on the (non-unique!) email field. Also, you don't need to use backticks to quote fields unless they contain special characters such as spaces.


You can use a built-in secure password hashing method instead of an external package.


A password hash will be fixed size, so you should store it in a column with the correct data type and length (some hashing functions return binary blobs rather than strings).

For this reason you also shouldn't limit the password length to the size of the database field — it's unrelated to the length of the password. You may still want to limit the password length to something sane like 1,000 characters, though.


SELECT * is discouraged in code - it makes schema changes harder, wastes bandwidth, and is less explicit about what is needed.


In general, input, output and persistence should be separate. If you rebuild this using TDD, making sure to dependency inject the database and user input handler, this should follow naturally.

\$\endgroup\$
  • \$\begingroup\$ Hello and thank you for your answer! I don't really know how to work with classes but I'll do my best in understanding them. I have a big problem in splitting display_info(), can you please tell me how can I simplify that function? \$\endgroup\$ – Maria Laura May 3 at 11:15
  • \$\begingroup\$ It is kinda hard to refactor without rebuilding from scratch. For example, len(dealer) makes no sense semantically - clearly dealer is not a dealer as such, but a list of something. Also, score(player) is weird - you calculate someone's score based on their cards, not on the person. I've added a paragraph at the end about handling this. \$\endgroup\$ – l0b0 May 3 at 22:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.