Created
August 1, 2024 18:00
-
-
Save nielsvaes/364d081bf29066de08d02b403d59b65e to your computer and use it in GitHub Desktop.
A very simple card counting blackjack simulator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import random | |
| import logging | |
| import matplotlib.pyplot as plt | |
| class ShouldLog: | |
| yes = logging.INFO | |
| no = logging.ERROR | |
| # Set up logging | |
| logging.basicConfig( | |
| filename='D:/bjhands.txt', | |
| level=ShouldLog.yes, | |
| format='%(message)s', | |
| encoding='utf-8' | |
| ) | |
| BET_MULTIPLIERS = { | |
| 1: 1, | |
| 2: 3, | |
| 3: 4, | |
| 4: 6 # This will be used for true count 4 and above | |
| } | |
| class Card: | |
| def __init__(self, rank, suit): | |
| self.rank = rank | |
| self.suit = suit | |
| self.value = self.get_value() | |
| def get_value(self): | |
| if self.rank in ['J', 'Q', 'K']: | |
| return 10 | |
| elif self.rank == 'A': | |
| return 11 | |
| else: | |
| return int(self.rank) | |
| def get_count_value(self): | |
| if self.rank in ['2', '3', '4', '5', '6']: | |
| return 1 | |
| elif self.rank in ['10', 'J', 'Q', 'K', 'A']: | |
| return -1 | |
| else: | |
| return 0 | |
| class Deck: | |
| def __init__(self, num_decks=6): | |
| self.num_decks = num_decks | |
| self.reshuffle() | |
| self.reshuffle_point = random.randint(60, 75) | |
| def reshuffle(self): | |
| ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'] | |
| suits = ['♠', '♥', '♦', '♣'] | |
| self.cards = [Card(rank, suit) for _ in range(self.num_decks) for rank in ranks for suit in suits] | |
| random.shuffle(self.cards) | |
| self.running_count = 0 | |
| logging.info(f"\n\n********************\nReshuffling the deck. Count reset to 0.\n********************\n\n") | |
| def draw(self): | |
| if not self.cards: | |
| self.reshuffle() | |
| card = self.cards.pop() | |
| self.running_count += card.get_count_value() | |
| return card | |
| def get_true_count(self): | |
| decks_remaining = max(len(self.cards) / 52, 1) # Ensure we don't divide by zero | |
| return self.running_count / decks_remaining | |
| def should_reshuffle(self): | |
| return len(self.cards) <= self.reshuffle_point | |
| class Hand: | |
| def __init__(self): | |
| self.cards = [] | |
| def add_card(self, card): | |
| self.cards.append(card) | |
| def get_value(self): | |
| value = sum(card.value for card in self.cards) | |
| num_aces = sum(1 for card in self.cards if card.rank == 'A') | |
| while value > 21 and num_aces > 0: | |
| value -= 10 | |
| num_aces -= 1 | |
| return value | |
| def is_blackjack(self): | |
| return len(self.cards) == 2 and self.get_value() == 21 | |
| def is_pair(self): | |
| return len(self.cards) == 2 and self.cards[0].rank == self.cards[1].rank | |
| def is_soft(self): | |
| ace_count = sum(1 for card in self.cards if card.rank == 'A') | |
| non_ace_sum = sum(card.value for card in self.cards if card.rank != 'A') | |
| return ace_count > 0 and (non_ace_sum + 10 + (ace_count - 1)) <= 21 | |
| def should_split(player_hand, dealer_upcard, true_count): | |
| if not player_hand.is_pair(): | |
| return False | |
| pair_rank = player_hand.cards[0].rank | |
| dealer_value = dealer_upcard.value | |
| split_rules = { | |
| 'A': True, | |
| 'T': dealer_value in [5, 6] and true_count >= 5, | |
| '9': dealer_value not in [7, 10, 11], | |
| '8': True, | |
| '7': dealer_value not in [8, 9, 10, 11], | |
| '6': dealer_value in range(2, 7) or (dealer_value == 7 and True), | |
| '5': False, | |
| '4': dealer_value in [5, 6] and True, | |
| '3': dealer_value in range(2, 8) and True, | |
| '2': dealer_value in range(2, 8) and True, | |
| } | |
| return split_rules.get(pair_rank, False) | |
| def should_double(player_hand, dealer_upcard, true_count): | |
| player_value = player_hand.get_value() | |
| dealer_value = dealer_upcard.value | |
| if player_hand.is_soft(): | |
| if player_value == 19 and dealer_value == 6: | |
| return True | |
| elif player_value == 18: | |
| return dealer_value in range(3, 7) | |
| elif player_value in [17, 16]: | |
| return dealer_value in range(2, 7) | |
| elif player_value in [15, 14]: | |
| return dealer_value in range(4, 7) | |
| elif player_value in [13, 12]: | |
| return dealer_value in [5, 6] | |
| else: | |
| if player_value == 11: | |
| return True | |
| elif player_value == 10: | |
| return dealer_value in range(2, 10) | |
| elif player_value == 9: | |
| return dealer_value in range(3, 7) or (dealer_value == 2 and true_count >= 1) | |
| return False | |
| def should_hit(player_hand, dealer_upcard, true_count): | |
| player_value = player_hand.get_value() | |
| dealer_value = dealer_upcard.value | |
| if player_hand.is_soft(): | |
| return should_hit_soft(player_value, dealer_value) | |
| else: | |
| return should_hit_hard(player_value, dealer_value, true_count) | |
| def should_hit_soft(player_value, dealer_value): | |
| return player_value <= 17 or (player_value == 18 and dealer_value in [9, 10, 11]) | |
| def should_hit_hard(player_value, dealer_value, true_count): | |
| if player_value >= 17: | |
| return False | |
| elif player_value == 16 and dealer_value == 10 and true_count <= 0: | |
| return True | |
| elif player_value >= 13: | |
| return dealer_value not in range(2, 7) | |
| elif player_value == 12: | |
| return dealer_value not in range(4, 7) | |
| else: | |
| return True | |
| def should_surrender(player_hand, dealer_upcard, true_count): | |
| player_value = player_hand.get_value() | |
| dealer_value = dealer_upcard.value | |
| return player_value == 15 and dealer_value == 10 and true_count <= 0 | |
| def play_hand(deck, base_bet): | |
| initial_count = deck.running_count | |
| true_count = deck.get_true_count() | |
| bet_multiplier = BET_MULTIPLIERS.get(min(int(true_count), max(BET_MULTIPLIERS.keys())), 1) | |
| bet = base_bet * bet_multiplier | |
| player_hand = Hand() | |
| dealer_hand = Hand() | |
| # Initial deal | |
| player_hand.add_card(deck.draw()) | |
| dealer_hand.add_card(deck.draw()) | |
| player_hand.add_card(deck.draw()) | |
| dealer_hand.add_card(deck.draw()) | |
| dealer_upcard = dealer_hand.cards[0] | |
| logging.info(f"Bet: ${bet}") | |
| logging.info(f"Player's hand: {[card.rank + card.suit for card in player_hand.cards]} => {player_hand.get_value()}") | |
| logging.info(f"Dealer's upcard: {dealer_upcard.rank + dealer_upcard.suit}") | |
| logging.info(f"Dealer's hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| # Check for blackjacks | |
| player_blackjack = player_hand.is_blackjack() | |
| dealer_blackjack = dealer_hand.is_blackjack() | |
| if player_blackjack or dealer_blackjack: | |
| if player_blackjack and dealer_blackjack: | |
| logging.info("Both player and dealer have blackjack. Push.") | |
| return 0 # Push | |
| elif player_blackjack: | |
| logging.info("Player has blackjack! Player wins 3:2.") | |
| return bet * 1.5 # Blackjack pays 3:2 | |
| else: | |
| logging.info("Dealer has blackjack. Player loses.") | |
| return -bet | |
| # Player's turn | |
| split_result = None | |
| if should_split(player_hand, dealer_upcard, true_count): | |
| logging.info("Player splits") | |
| split_result, _ = play_split_hands(deck, bet, player_hand.cards[0], dealer_upcard, true_count) | |
| return split_result # Return the split result immediately | |
| elif should_double(player_hand, dealer_upcard, true_count): | |
| logging.info("Player doubles") | |
| player_hand.add_card(deck.draw()) | |
| logging.info(f"Player's hand after doubling: {[card.rank + card.suit for card in player_hand.cards]} => {player_hand.get_value()}") | |
| bet *= 2 | |
| else: | |
| while should_hit(player_hand, dealer_upcard, true_count): | |
| logging.info("Player hits") | |
| player_hand.add_card(deck.draw()) | |
| logging.info(f"Player's hand after hitting: {[card.rank + card.suit for card in player_hand.cards]} => {player_hand.get_value()}") | |
| if player_hand.get_value() > 21: | |
| logging.info("Player busts") | |
| return -bet | |
| logging.info("Player stands") | |
| # Dealer's turn | |
| logging.info("Dealer's turn") | |
| while dealer_hand.get_value() < 17 or (dealer_hand.get_value() == 17 and dealer_hand.is_soft()): | |
| dealer_hand.add_card(deck.draw()) | |
| logging.info(f"Dealer hits. New hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| logging.info(f"Dealer's final hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| result = compare_hands(player_hand, dealer_hand, bet) | |
| # Log the final running count | |
| final_count = deck.running_count | |
| logging.info(f"Initial count: {initial_count}") | |
| logging.info(f"Final count: {final_count}") | |
| logging.info(f"Count difference: {final_count - initial_count}") | |
| return result | |
| def play_split_hands(deck, bet, split_card, dealer_upcard, true_count): | |
| hands = [Hand(), Hand()] | |
| hands[0].add_card(split_card) | |
| hands[1].add_card(split_card) | |
| hands[0].add_card(deck.draw()) | |
| hands[1].add_card(deck.draw()) | |
| dealer_hand = Hand() | |
| dealer_hand.add_card(dealer_upcard) | |
| dealer_hand.add_card(deck.draw()) | |
| total_winnings = 0 | |
| cards_drawn = 3 # Initial cards drawn: 2 for player's split hands, 1 for dealer's second card | |
| # Check for dealer blackjack if upcard is 10, J, Q, K, or A | |
| if dealer_upcard.value in [10, 11]: | |
| logging.info("Checking for dealer blackjack...") | |
| if dealer_hand.is_blackjack(): | |
| logging.info(f"Dealer's hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| logging.info("Dealer has blackjack. Both split hands lose.") | |
| return -2 * bet, cards_drawn | |
| # Continue with the rest of the function only if dealer doesn't have blackjack | |
| for i, hand in enumerate(hands, 1): | |
| logging.info(f"Playing split hand {i}: {[card.rank + card.suit for card in hand.cards]} => {hand.get_value()}") | |
| while True: | |
| if should_double(hand, dealer_upcard, true_count): | |
| logging.info(f"Player doubles on split hand {i}") | |
| hand.add_card(deck.draw()) | |
| cards_drawn += 1 | |
| logging.info(f"Split hand {i} after doubling: {[card.rank + card.suit for card in hand.cards]} => {hand.get_value()}") | |
| break | |
| elif should_hit(hand, dealer_upcard, true_count): | |
| logging.info(f"Player hits on split hand {i}") | |
| hand.add_card(deck.draw()) | |
| cards_drawn += 1 | |
| logging.info(f"Split hand {i} after hitting: {[card.rank + card.suit for card in hand.cards]} => {hand.get_value()}") | |
| if hand.get_value() > 21: | |
| logging.info(f"Player busts on split hand {i}") | |
| total_winnings -= bet | |
| break | |
| else: | |
| logging.info(f"Player stands on split hand {i}") | |
| break | |
| # Dealer's turn (only played once for both split hands) | |
| if any(hand.get_value() <= 21 for hand in hands): | |
| while dealer_hand.get_value() < 17 or (dealer_hand.get_value() == 17 and dealer_hand.is_soft()): | |
| dealer_hand.add_card(deck.draw()) | |
| cards_drawn += 1 | |
| logging.info(f"Dealer hits. New hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| else: | |
| logging.info(f"Both split hands bust. Dealer's hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| logging.info(f"Dealer's final hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| # Compare each split hand with the dealer's hand | |
| for i, hand in enumerate(hands, 1): | |
| if hand.get_value() <= 21: # Only compare if the hand didn't bust | |
| result = compare_hands(hand, dealer_hand, bet) | |
| total_winnings += result | |
| logging.info(f"Split hand {i} result: ${result}") | |
| return total_winnings, cards_drawn | |
| def play_dealer_hand(deck, dealer_upcard): | |
| dealer_hand = Hand() | |
| dealer_hand.add_card(dealer_upcard) | |
| dealer_hand.add_card(deck.draw()) | |
| logging.info("Dealer's turn") | |
| logging.info(f"Dealer's initial hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| while dealer_hand.get_value() < 17 or (dealer_hand.get_value() == 17 and dealer_hand.is_soft()): | |
| dealer_hand.add_card(deck.draw()) | |
| logging.info(f"Dealer hits. New hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| logging.info(f"Dealer's final hand: {[card.rank + card.suit for card in dealer_hand.cards]} => {dealer_hand.get_value()}") | |
| return dealer_hand | |
| def compare_hands(player_hand, dealer_hand, bet): | |
| player_value = player_hand.get_value() | |
| dealer_value = dealer_hand.get_value() | |
| if dealer_value > 21: | |
| logging.info("Dealer busts, player wins") | |
| return bet | |
| elif player_value > dealer_value: | |
| logging.info("Player wins") | |
| return bet | |
| elif player_value < dealer_value: | |
| logging.info("Dealer wins") | |
| return -bet | |
| else: | |
| logging.info("Push") | |
| return 0 | |
| def simulate_blackjack(num_hands, initial_bankroll, base_bet): | |
| deck = Deck(num_decks=6) | |
| bankroll = initial_bankroll | |
| player_wins = 0 | |
| dealer_wins = 0 | |
| pushes = 0 | |
| bankroll_history = [initial_bankroll] # Initialize bankroll history | |
| for hand_num in range(1, num_hands + 1): | |
| logging.info(f"\nHand #{hand_num}") | |
| logging.info(f"=" * len(f"Hand #{hand_num}")) | |
| logging.info(f"Current bankroll: ${bankroll}") | |
| logging.info(f"Running count: {deck.running_count}") | |
| logging.info(f"True count: {deck.get_true_count():.2f}") | |
| result = play_hand(deck, base_bet) | |
| bankroll += result | |
| bankroll_history.append(bankroll) # Add current bankroll to history | |
| if result > 0: | |
| player_wins += 1 | |
| elif result < 0: | |
| dealer_wins += 1 | |
| else: | |
| pushes += 1 | |
| logging.info(f"Running count: {deck.running_count}") | |
| logging.info(f"True count: {deck.get_true_count():.2f}") | |
| logging.info(f"Hand result: ${result}") | |
| logging.info(f"New bankroll: ${bankroll}") | |
| # Check if we need to reshuffle after the hand is completed | |
| if deck.should_reshuffle(): | |
| deck.reshuffle() | |
| deck.reshuffle_point = random.randint(60, 75) # Reset reshuffle point after shuffling | |
| return bankroll, player_wins, dealer_wins, pushes, bankroll_history | |
| # Add a new function to plot the bankroll history | |
| def plot_bankroll_history(bankroll_history, num_hands, initial_bankroll): | |
| plt.figure(figsize=(12, 6)) | |
| plt.plot(range(num_hands + 1), bankroll_history) | |
| plt.title(f"Bankroll Over Time (Initial: ${initial_bankroll})") | |
| plt.xlabel("Number of Hands") | |
| plt.ylabel("Bankroll ($)") | |
| plt.grid(True) | |
| # Find all-time high, all-time low, and final bankroll | |
| all_time_high = max(bankroll_history) | |
| all_time_low = min(bankroll_history) | |
| final_bankroll = bankroll_history[-1] | |
| # Find the indices (hand numbers) where these values occur | |
| high_index = bankroll_history.index(all_time_high) | |
| low_index = bankroll_history.index(all_time_low) | |
| # Add labels for all-time high, all-time low, and final bankroll | |
| plt.annotate(f'All-time high: ${all_time_high}', | |
| xy=(high_index, all_time_high), | |
| xytext=(10, 10), | |
| textcoords='offset points', | |
| ha='left', | |
| va='bottom', | |
| bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5), | |
| arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')) | |
| plt.annotate(f'All-time low: ${all_time_low}', | |
| xy=(low_index, all_time_low), | |
| xytext=(10, -10), | |
| textcoords='offset points', | |
| ha='left', | |
| va='top', | |
| bbox=dict(boxstyle='round,pad=0.5', fc='red', alpha=0.5), | |
| arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')) | |
| plt.annotate(f'Final: ${final_bankroll}', | |
| xy=(num_hands, final_bankroll), | |
| xytext=(-10, 0), | |
| textcoords='offset points', | |
| ha='right', | |
| va='center', | |
| bbox=dict(boxstyle='round,pad=0.5', fc='green', alpha=0.5), | |
| arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')) | |
| plt.tight_layout() | |
| plt.savefig('bankroll_history.png', dpi=300) | |
| plt.close() | |
| # Update the main script | |
| if __name__ == "__main__": | |
| num_hands = 60000 | |
| initial_bankroll = 50000 | |
| base_bet = 15 | |
| logging.info("Starting Blackjack simulation with Hi-Lo card counting") | |
| logging.info(f"Initial parameters: {num_hands} hands, ${initial_bankroll} initial bankroll, ${base_bet} base bet") | |
| final_bankroll, player_wins, dealer_wins, pushes, bankroll_history = simulate_blackjack(num_hands, initial_bankroll, base_bet) | |
| logging.info("\nSimulation Results:") | |
| logging.info(f"Final bankroll after {num_hands} hands: ${final_bankroll}") | |
| logging.info(f"Player wins: {player_wins}") | |
| logging.info(f"Dealer wins: {dealer_wins}") | |
| logging.info(f"Pushes: {pushes}") | |
| print(f"Simulation complete. Results logged to D:/bjhands.txt") | |
| print(f"Final bankroll after {num_hands} hands: ${final_bankroll}") | |
| print(f"Player wins: {player_wins}") | |
| print(f"Dealer wins: {dealer_wins}") | |
| print(f"Pushes: {pushes}") | |
| # Plot and save the bankroll history | |
| plot_bankroll_history(bankroll_history, num_hands, initial_bankroll) | |
| print("Bankroll history graph saved as 'bankroll_history.png'") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment