# Lastest version of my Blackjack game

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

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}')
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()
checked = check_account(cur, email)
if checked:
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;

• Can you link to the previous post? – l0b0 May 3 '19 at 2:06
• – Maria Laura May 3 '19 at 8:27

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.

• 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? – Maria Laura May 3 '19 at 11:15
• 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. – l0b0 May 3 '19 at 22:08