Skip to content

Instantly share code, notes, and snippets.

@nielsvaes
Created August 1, 2024 18:00
Show Gist options
  • Select an option

  • Save nielsvaes/364d081bf29066de08d02b403d59b65e to your computer and use it in GitHub Desktop.

Select an option

Save nielsvaes/364d081bf29066de08d02b403d59b65e to your computer and use it in GitHub Desktop.
A very simple card counting blackjack simulator
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