import pandas as pd import numpy as np import random import statistics import matplotlib.pyplot as plt import seaborn as sns random.seed(123)
Note: The code that I use to run the Monte Carlo simulations outlined below can be found on Github here.
Seven and a half is a card game that’s something like 2/3rds blackjack and 1/3 poker. It’s known as setto e mezzo in Italy or Siete y Media in Spain (that literally just means 7.5 in Italian/Spanish, but the point being it is popular in these other countries) and is played with a deck of 40 cards (standard deck of cards, minus the 8s, 9s, and 10s). Players receive one card face down (as does the dealer), and a player’s goal is to beat the dealer by getting a hand as close to 7 1/2 as possible without busting (going over 7.5). Face cards are worth 1/2 point and all other cards are worth their face value. A player can hit until they either choose to stay or they bust, at which point the dealer can then hit until they choose to stay or bust as well. Ties go to the dealer. When I have played this game, a player doesn’t need to place their bet on their hand until receiving their first card, though I understand this may not be traditional.
After playing the game a few times, I’ve started to get curious on what ideal gameplay actually looks like. Since this isn’t a game of perfect information (both the player and the dealer have one face-down card, so an element of bluffing – the poker aspect – is certainly at play), you can’t necessarily solve for a true dominant strategy, but it’s at least possible to map out what would be ideal gameplay if all information was known in each stage. Here’s an attempt to do that (and a ‘fun’ opportunity to walk through a Monte Carlo simulation in Python).
The goal of the game is to beat the dealer, but to start off, let’s just see what happens if your goal is just to maximize your hand, regardless of what any other player has. We’ll use a basic “hit until you reach a score of X” strategy, change what X is, and see what X gives you the highest expected value if we simulate a million hands for each value of X:
overall_scores =  for i in range(15): threshold = (0.5 + i * 0.5) final_scores =  for j in range(1000000): hand =  deck = [2, 3, 4, 5, 6, 7, 1, 0.5, 0.5, 0.5] * 4 random.shuffle(deck) hand.append(deck.pop()) score = sum(hand) while score < threshold: hand.append(deck.pop()) score = sum(hand) if score > 7.5: final_score = 0 else: final_score = score final_scores.append(final_score) overall_scores.append([threshold, final_scores])
score_data = pd.DataFrame(dict(overall_scores)).mean() scores = pd.DataFrame(score_data).reset_index() scores.columns = ['Threshold', 'Mean Score'] scores
Alright, so the strategy with the highest expected hand value is “hit until your hand is at least 3.5 or 4.0,” which seems pretty reasonable, considering that’s about halfway to the max score of 7.5 (reasonably high, but not so high that you’ll bust very frequently).
Now let’s look at the two-player version of the game (Player versus Dealer) and work backwards a little. Assume you’re the dealer, and you know a player has X. Your goal is to at least tie X, which means your strategy is simply to keep hitting until your score is at least X. If you bust before you hit X, you lose. If you don’t, you win. What’re your odds of winning? Here I’ll again simulate a million hands for each X and see what we get:
overall_scores =  for i in range(15): threshold = (0.5 + i * 0.5) final_scores =  for j in range(1000000): hand =  deck = [2, 3, 4, 5, 6, 7, 1, 0.5, 0.5, 0.5] * 4 random.shuffle(deck) hand.append(deck.pop()) score = sum(hand) while score < threshold: hand.append(deck.pop()) score = sum(hand) if score > 7.5: final_score = 0 else: final_score = 1 final_scores.append(final_score) overall_scores.append([threshold, final_scores])
dealer_wins = pd.DataFrame(dict(overall_scores)).mean() dealer_win = pd.DataFrame(dealer_wins).reset_index() dealer_win.columns = ['Threshold', 'Dealer Win %'] dealer_win
|Threshold||Dealer Win %|
Results seem pretty straightforward – the dealer’s odds of winning decrease as the score he has to beat goes up – and the odds are falling a bit less than linearly as we go – but it’s actually pretty interesting to me to see how favored the dealer is in this game. Based on this, the dealer is favored to win for any player score under 6.5, which I think suggests a couple things. First, as the player it pays to be aggressive; even if score-maximizing in the abstract happens with a cutoff of 3.5 /4 or so, you’re probably going to lose as the player unless you keep hitting. Second, bluffing clearly matters; whatever you can do to throw the dealer off his game will help you in a big way, and you need it.
Okay, now let’s take a step back and think about this from the player’s perspective. Every time you decide whether to hit, you impact the probability that you win the game in one of two ways:
- You bust with some probability, which results in a 100% chance the dealer wins
- Conditional upon not busting, you lower the dealer’s chances of winning by some probability
For example, say you start with an Ace, so you hand is worth 1. The table above shows a dealer with perfect information has a 100% chance of winning here. If you draw a 7, you bust, so your odds don’t get any worse – you still lose. If you draw a 6, your hand is now worth 7, so the dealer has a 35% chance of winning – your odds of winning have improved by 65 percentage points.
As a player, your goal is to maximize the probability you have of winning the game. Phrased differently, your goal is to minimize the probability the dealer has of winning the game. As such, a player should hit only when doing so increases their chances of winning (and decreases the dealer’s chances). Since we know the probability the dealer has of winning given every player score, we can then figure out exactly how a hit is expected to impact the player’s probability of winning, which tells us when the player should / should not hit.
Given that, here’s what we’ll do:
- Take the inverse of ‘dealer win probability’ as ‘player win probability’
- Start at each possible score (0.5 to 7.5), and simulate a million hands where you take a hit.
- Compare starting win probability to average ending win probability to see where a hit improves your win probability.
player_win_prob = (1 - dealer_wins) player_outcomes =  for i in range(15): start = (0.5 + i * 0.5) final_scores =  start_probs =  end_probs =  bust_count =  for j in range(1000000): hand = [start] start_win_probability = player_win_prob[start] deck = [2, 3, 4, 5, 6, 7, 1, 0.5, 0.5, 0.5] * 4 random.shuffle(deck) hand.append(deck.pop()) final_score = sum(hand) try: end_win_probability = player_win_prob[final_score] except: end_win_probability = 0 if final_score > 7.5: bust = 1 else: bust = 0 start_probs.append(start_win_probability) end_probs.append(end_win_probability) final_scores.append(final_score) bust_count.append(bust) player_outcomes.append([start, statistics.mean(bust_count), statistics.mean(final_scores), statistics.mean(start_probs), statistics.mean(end_probs)])
player_wins = pd.DataFrame((player_outcomes)) player_wins.columns = ['score_start', 'bust_prob', 'score_end_avg', 'win_prob_start', 'win_prob_end'] player_wins['score_increase'] = player_wins['score_end_avg'] - player_wins['score_start'] player_wins['prob_change'] = player_wins['win_prob_end'] - player_wins['win_prob_start'] player_wins
Pretty straightforward! Looks like the player should hit if their hand is anything below 5 – above there, hitting has negative value (the odds of busting out are too great).
Astute cardplayers will notice that the above is a bit simplistic – I’m pretending the entire deck of cards is in play when you take a hit, when in reality at least one (or two) cards have to have been played already to make the player’s starting hand. That’s totally fair, but:
- I think the actual analysis there is beyond the scope of what I can put together,
- For someone not counting cards, in practice you won’t really be able to adjust your math on the fly sufficiently to use a more sophisticated version, and
- In theory, if someone were playing with an infinite number of decks, the math above is still right, so let’s go with it.
Now – last step. Given the above, we know the dealer’s odds of winning a hand based on where the player ends up, and we know how the player should act given those odds (hit until 5 or higher, then stay). My last question is how the player’s odds of winning vary based on the card they’re dealt. Intuitively, I’d expect 6s and 7s to be great cards (clearly if you get a 7 off the bat, you’ve got nearly a 2/3rds chance of winning, which is great), but I’m not sure whether a face card (0.5) or an Ace (1) will be a lot better than a 3 or a 4 – your starting score is lower, but your odds of busting are a lot better.
Let’s take a shot at figuring out the best cards by simulating a million hands using the “hit until you reach 5 or better” strategy, and see how often the player wins at each starting card:
overall_outcomes =  for i in range(1000000): player_hand =  dealer_hand =  deck = [2, 3, 4, 5, 6, 7, 1, 0.5, 0.5, 0.5] * 4 random.shuffle(deck) player_hand.append(deck.pop()) player_first_card = player_hand dealer_hand.append(deck.pop()) player_score = sum(player_hand) dealer_score = sum(dealer_hand) while player_score < 5: player_hand.append(deck.pop()) player_score = sum(player_hand) if player_score > 7.5: player_final_score = 0 else: player_final_score = player_score while dealer_score < player_final_score: dealer_hand.append(deck.pop()) dealer_score = sum(dealer_hand) if dealer_score > 7.5: dealer_final_score = 0 else: dealer_final_score = dealer_score if dealer_final_score >= player_final_score: outcome = 0 else: outcome = 1 overall_outcomes.append([player_first_card, outcome])
result = pd.DataFrame(overall_outcomes, columns = ['first_card', 'outcome']) summary = result.groupby('first_card').agg(['mean', 'count']).reset_index() summary.columns = ['First Card', 'Win Probability', 'Count'] summary
|First Card||Win Probability||Count|
Not surprisingly, 7 was confirmed best (about a 2:1 favorite to win) – but the rest is pretty interesting. First off, a 6 is actually pretty bad! Only a 44% chance to win, and that’s the second best odds of any card in the deck. After 6, the next best starting card is actually 0.5 by a pretty wide margin – it’s almost twice as good as starting with a 3 or a 4, and isn’t much worse than a 6 (39% chance to win versus 44%). Also notably, the player’s overall odds of winning a hand here (before knowing their card) are ~35%.
So, to recap:
- In the abstract, hitting until 3.5 or 4 gets you the highest average hand
- Since the dealer goes second, and this analysis assumes he knows what he has to get, hitting until 5 is the best strategy.
- The player is a pretty big underdog here, with ~35% chance of winning in a perfect information setting. Unless they’re dealt a 7, the dealer is favored, and that doesn’t change unless they’ve got at least 6.5 in their hand.
- Given the above, it’s obvious how important the lack of perfect information is in this game – bluffing is huge for the player. In terms of optimal betting strategy you should bet big if and only if you get a 7, unless you’re trying to bluff. Conversely, if you’re the dealer, you’re not really afraid of a player betting big unless you’re pretty sure they have a 7 – anything else and you’re favored to win the hand.
All in all, this was a pretty fun and illuminating exercise – and I feel a bit better about how to approach the game going forward.