pythonfor-loopblackjack

Handling card splitting in Blackjack without modifying a list during iteration


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.


Solution

  • 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:

    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)