I'm learning Python and working on a self-assigned card game project - Blackjack. According to the rules, if a player has an initial hand where the points on each card is the same, the player can split his hand into two; one card is dealt to each split hand which then becomes two hands.
This is the Wikipedia link for the rules of Blackjack that I followed. https://en.wikipedia.org/wiki/Blackjack
This is what I want to achieve: Check the player's initial hand to see if both cards have the same point value; if yes, give the player the option to split his cards. If he chooses to split, create two hands from the initial hand. Once again, check the point values of the split hands and follow the same process until the cards can't be split or the player chooses not to split or has maxed out at 3 splits (equivalent to 4 hands). The code begins with accepting a list representing the initial hand and I want it to output a list of upto 4 lists representing the final hands after splitting.
I have solved the problem but it involves changing a list while iterating over it using a for loop. I wanted to add items to the end of the list while the loop was running and the code is working; however, I know this practice should be avoided as a general rule of thumb so I'm looking for an alternative solution.
I think I need a recursive function but my concept of this is not very clear.
An extract of my code is given below.
import random
def split_hand(cards):
"""Takes the player's cards as an argument and returns 2 hands of two cards each, i.e.
a list of two lists of two cards each"""
split_cards = [[cards[i]] for i in range(2)]
for card in split_cards:
card.append(random.choice(test_card_deck))
return split_cards
test_card_deck = ["10 of Spades", "6 of Clubs", "Queen of Clubs", "Jack of Diamonds"]
# Dictionary of the cards with their corresponding points
test_card_dict = {"10 of Spades": 10, "6 of Clubs": 6, "Queen of Clubs": 10, "Jack of Diamonds": 10}
# Deal an initial hand to the player
player_cards = ["10 of Spades", "Queen of Clubs"]
# Initialise variables
final_hands = []
hands = [player_cards]
split_counter = 0
"""This is working but the code is iffy because I am modifying the list (hands) while it is being iterated over; alternative needed"""
for hand in hands:
if test_card_dict[hand[0]] != test_card_dict[hand[1]]:
final_hands.append(hand)
else:
if split_counter < 3:
print(f"\nYour cards: [{", ".join(hand)}]")
split = input("Would you like to split? Type 'y' for yes or 'n' for no: ")
if split == "y":
split_counter += 1 # Increment counter every time a hand is split
result_of_split = split_hand(hand)
for each_set_of_cards in result_of_split:
hands.append(each_set_of_cards) # I wanted the split cards to get added on at the end of 'hands' and be processed
else:
final_hands.append(hand)
else:
final_hands.append(hand)
print(final_hands)
This is my first question here; I hope I've been able to describe my issue and what I want to achieve. Looking for suggestions/alternative directions/troubleshooting... any help I can get. Thanks in advance.
Although in some cases it may be a problem to append to the list you are also iterating, it is not always a bad practice. This is actually a practice that is common with queues.
Some other remarks on your code:
Using random.choice
to pick a card from the deck will allow the same card to be dealt any number of times. This is not realistic. Better is to shuffle the deck, and then pop cards from that shuffled deck as you need them.
Related to the previous remark: the test deck is not large enough to allow distinct cards to be dealt in the case where 3 splits occur, which means you could end up with 4 hands, i.e. 8 cards. So your test deck should at least have that many cards to be realistic.
In two places you have a loop to append all items of one list to another list, but you can use extend
for that purpose.
It is not good practice to mix I/O handling (such as asking for input) with game logic. It would improve if you would put that I/O handling in a separate function
There are three spots where you do final_hands.append(hand)
. It would be more elegant to avoid this code repetition and deal with all these cases in one branch of your code.
card_deck
and card_list
duplicate the same names: this repetition can be avoided.
If we take the idea of a queue (using deque
) and the above remarks into account, the code could look like this:
import random
from collections import deque
def split_hand(hand):
"""Takes the player's cards as an argument and returns 2 hands of two cards each, i.e.
a list of two lists of two cards each"""
# Avoid taking the same card twice: use pop, and use it on the fly
return [[card, card_deck.pop()] for card in hand]
def confirm_split(hand): # isolate I/O handling
"""Asks the user to give their choice on whether to split or not"""
print(f"\nYour cards: [{", ".join(hand)}]")
split = input("Would you like to split? Type 'y' for yes or 'n' for no: ")
return split == "y"
# Use a larger test deck, as three splits means there will be 4 hands of 2, i.e. 8 cards
# Dictionary of the cards with their corresponding points
card_dict = {"10 of Spades": 10, "6 of Clubs": 6, "Queen of Clubs": 10, "Jack of Diamonds": 10,
"King of Hearts": 10, "6 of Spades": 6, "Queen of Hearts": 10, "10 of Clubs": 10 }
card_deck = list(card_dict.keys()) # No need to repeat yourself here
random.shuffle(card_deck) # This is where you introduce the random aspect
# Deal an initial hand to the player: use pop
hand = [card_deck.pop(), card_deck.pop()]
final_hands = []
pending_hands = deque([hand]) # use a FIFO queue
while pending_hands and len(final_hands) + len(pending_hands) < 4:
hand = pending_hands.popleft()
if card_dict[hand[0]] != card_dict[hand[1]] or not confirm_split(hand):
final_hands.append(hand)
else:
pending_hands.extend(split_hand(hand)) # use extend instead of loop
final_hands.extend(pending_hands) # Flush any remaining hands
print(final_hands)