-
-
Save philippdubach/741cbd56498e43375892966ca691b9c2 to your computer and use it in GitHub Desktop.
Minimal blackjack odds calculator returns win probabilities using Monte Carlo simulation and recommended next move (Hit, Stand, Double, Split).
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 | |
| from collections import Counter | |
| class BlackjackAdvisor: | |
| def __init__(self): | |
| # Basic strategy chart: [player_total][dealer_upcard] = action | |
| # Actions: 'H'=Hit, 'S'=Stand, 'D'=Double, 'P'=Split | |
| self.basic_strategy = { | |
| # Hard totals (no aces counted as 11) | |
| 'hard': { | |
| 5: ['H'] * 10, 6: ['H'] * 10, 7: ['H'] * 10, 8: ['H'] * 10, | |
| 9: ['H','H','D','D','D','H','H','H','H','H'], | |
| 10: ['D','D','D','D','D','D','D','D','H','H'], | |
| 11: ['D','D','D','D','D','D','D','D','D','H'], | |
| 12: ['H','H','S','S','S','H','H','H','H','H'], | |
| 13: ['S','S','S','S','S','H','H','H','H','H'], | |
| 14: ['S','S','S','S','S','H','H','H','H','H'], | |
| 15: ['S','S','S','S','S','H','H','H','H','H'], | |
| 16: ['S','S','S','S','S','H','H','H','H','H'], | |
| 17: ['S'] * 10, 18: ['S'] * 10, 19: ['S'] * 10, 20: ['S'] * 10, 21: ['S'] * 10 | |
| }, | |
| # Soft totals (ace counted as 11) | |
| 'soft': { | |
| 13: ['H','H','H','D','D','H','H','H','H','H'], # A,2 | |
| 14: ['H','H','H','D','D','H','H','H','H','H'], # A,3 | |
| 15: ['H','H','D','D','D','H','H','H','H','H'], # A,4 | |
| 16: ['H','H','D','D','D','H','H','H','H','H'], # A,5 | |
| 17: ['H','D','D','D','D','H','H','H','H','H'], # A,6 | |
| 18: ['S','D','D','D','D','S','S','H','H','H'], # A,7 | |
| 19: ['S'] * 10, 20: ['S'] * 10, 21: ['S'] * 10 # A,8, A,9, A,10 | |
| }, | |
| # Pairs | |
| 'pairs': { | |
| 2: ['P','P','P','P','P','P','H','H','H','H'], # 2,2 | |
| 3: ['P','P','P','P','P','P','H','H','H','H'], # 3,3 | |
| 4: ['H','H','H','P','P','H','H','H','H','H'], # 4,4 | |
| 5: ['D','D','D','D','D','D','D','D','H','H'], # 5,5 (treat as 10) | |
| 6: ['P','P','P','P','P','H','H','H','H','H'], # 6,6 | |
| 7: ['P','P','P','P','P','P','H','H','H','H'], # 7,7 | |
| 8: ['P','P','P','P','P','P','P','P','H','H'], # 8,8 | |
| 9: ['P','P','P','P','P','S','P','P','S','S'], # 9,9 | |
| 10: ['S'] * 10, # 10,10 (never split) | |
| 11: ['P'] * 10 # A,A | |
| } | |
| } | |
| def card_value(self, card): | |
| """Convert card to numerical value""" | |
| if card in ['J', 'Q', 'K']: | |
| return 10 | |
| elif card == 'A': | |
| return 11 | |
| return int(card) | |
| def hand_value(self, cards): | |
| """Calculate hand value, handling aces""" | |
| values = [self.card_value(card) for card in cards] | |
| total = sum(values) | |
| aces = values.count(11) | |
| # Convert aces from 11 to 1 if needed | |
| while total > 21 and aces > 0: | |
| total -= 10 | |
| aces -= 1 | |
| return total, aces > 0 # Return (value, is_soft) | |
| def is_pair(self, cards): | |
| """Check if hand is a pair""" | |
| if len(cards) != 2: | |
| return False | |
| return self.card_value(cards[0]) == self.card_value(cards[1]) | |
| def get_basic_strategy(self, player_cards, dealer_upcard): | |
| """Get basic recommendation""" | |
| player_total, is_soft = self.hand_value(player_cards) | |
| dealer_value = self.card_value(dealer_upcard) | |
| dealer_idx = dealer_value - 2 if dealer_value <= 10 else 9 # A=9, others 2-10 = 0-8 | |
| # Check for pair first | |
| if self.is_pair(player_cards): | |
| pair_value = self.card_value(player_cards[0]) | |
| if pair_value == 11: # Ace pair | |
| pair_value = 11 | |
| return self.basic_strategy['pairs'][pair_value][dealer_idx] | |
| # Soft hand (ace counted as 11) | |
| if is_soft and player_total <= 21: | |
| return self.basic_strategy['soft'][player_total][dealer_idx] | |
| # Hard hand | |
| player_total = min(player_total, 21) # Cap at 21 for lookup | |
| player_total = max(player_total, 5) # Minimum lookup value | |
| return self.basic_strategy['hard'][player_total][dealer_idx] | |
| def calculate_win_probability(self, player_cards, dealer_upcard, seen_cards=None): | |
| """Monte Carlo simulation for win probability""" | |
| if seen_cards is None: | |
| seen_cards = [] | |
| # Create remaining deck | |
| full_deck = (['A'] + [str(i) for i in range(2, 11)] + ['J', 'Q', 'K']) * 4 | |
| remaining_deck = full_deck.copy() | |
| # Remove seen cards | |
| for card in seen_cards + player_cards + [dealer_upcard]: | |
| if card in remaining_deck: | |
| remaining_deck.remove(card) | |
| wins = 0 | |
| simulations = 1000 | |
| for _ in range(simulations): | |
| # Simulate dealer hand | |
| dealer_cards = [dealer_upcard] | |
| deck_copy = remaining_deck.copy() | |
| random.shuffle(deck_copy) | |
| # Dealer plays | |
| while True: | |
| dealer_total, dealer_soft = self.hand_value(dealer_cards) | |
| if dealer_total > 21: | |
| break | |
| if dealer_total >= 17: | |
| if dealer_total == 17 and dealer_soft: | |
| # Dealer hits soft 17 in some casinos | |
| pass | |
| else: | |
| break | |
| if deck_copy: | |
| dealer_cards.append(deck_copy.pop()) | |
| else: | |
| break | |
| # Compare hands | |
| player_total, _ = self.hand_value(player_cards) | |
| dealer_total, _ = self.hand_value(dealer_cards) | |
| if dealer_total > 21: | |
| wins += 1 | |
| elif player_total > 21: | |
| wins += 0 | |
| elif player_total > dealer_total: | |
| wins += 1 | |
| elif player_total == dealer_total: | |
| wins += 0.5 # Push | |
| return wins / simulations | |
| def get_advice(self, player_cards, dealer_upcard, seen_cards=None): | |
| """Recommend next best move""" | |
| if seen_cards is None: | |
| seen_cards = [] | |
| player_total, is_soft = self.hand_value(player_cards) | |
| basic_action = self.get_basic_strategy(player_cards, dealer_upcard) | |
| win_prob = self.calculate_win_probability(player_cards, dealer_upcard, seen_cards) | |
| actions = { | |
| 'H': 'Hit', | |
| 'S': 'Stand', | |
| 'D': 'Double Down', | |
| 'P': 'Split' | |
| } | |
| return { | |
| 'player_total': player_total, | |
| 'is_soft': is_soft, | |
| 'basic_strategy': actions[basic_action], | |
| 'win_probability': f"{win_prob:.2%}", | |
| 'recommendation': actions[basic_action] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment