-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathblackjack.py
521 lines (400 loc) · 24.1 KB
/
blackjack.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# Blackjack simulator.
import random
# See - https://en.wikipedia.org/wiki/Standard_52-card_deck
class Deck:
# Create a new deck of cards.
def __init__(self):
# [(name of the rank, [list of possible values], card_value)]
# For example, ("2", [2]), ("K", [10]), ("A", [1, 11])
ranks = []
# Add the pip ranks, 2 through 10.
for i in range(2, 11):
ranks.append((str(i), [i], str(i)))
# Add the court ranks, Jack, Queen, King.
court = ["J", "Q", "K"]
for c in court:
ranks.append((c, [10], "10"))
# Add the Ace.
ranks.append(("A", [1, 11], "A"))
# Make 1 deck of cards.
self.cards = []
suits = ["♣", "♦", "♥", "♠"]
for r in ranks:
(name, value_list, card_value) = r
for s in suits:
card_name = name + s # For example "8" + "♥".
self.cards.append((card_name, value_list, card_value))
# Shuffle the deck.
def shuffle(self):
random.shuffle(self.cards)
class Card_Counting:
def __init__(self):
self.running_count = 0
self.true_count = 0
def reset(self):
self.running_count = 0
self.true_count = 0
self.bet_size = 0
def print(self):
print("Running count=%d, True count=%d, Bet size=%d" % (self.running_count, self.true_count, self.bet_size))
def adjust_count(self, card, shoe_size, betting_unit):
(_, _, card_value) = card
count_map = {"2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 0, "8": 0, "9": 0, "10": -1, "A": -1}
self.running_count += count_map.get(card_value) # Adjust the running count based on value of this card.
decks_in_shoe = shoe_size / 52
self.true_count = int(self.running_count / decks_in_shoe)
self.bet_size = (self.true_count - 1) * betting_unit
if self.bet_size < 0: # Can't bet a -ve stake :)
self.bet_size = 0
# See - https://en.wikipedia.org/wiki/Shoe_(cards)
class Shoe:
def __init__(self):
self.cards = [] # Cards currently in the shoe.
# Default values for these two attributes.
self.decks = 4 # Number of decks in the shoe when full.
self.penetration = 75 / 100 # Percentage of cards to be dealt before refilling shoe.
self.card_count = Card_Counting() # Create an object to do card counting.
# Print the first 10 cards in shoe, plus number of cards in shoe.
def print(self):
print("Shoe: ", end="")
for c in self.cards[0:10]:
(card, value_list, card_value) = c
print(card, end=" ")
print("Number of cards", len(self.cards))
# Shuffle all of the cards in the shoe.
def shuffle(self):
random.shuffle(self.cards)
# If the shoe of cards is getting small, then replenish it.
def replenish(self):
if len(self.cards) <= ((1 - self.penetration) * self.decks * 52):
self.cards = [] # Remove the remaining cards from the shoe.
for d in range(self.decks):
new_deck = Deck()
self.cards = self.cards + new_deck.cards
self.shuffle() # If cards were added, then shuffle the cards in the shoe.
self.card_count.reset() # Reset the card count to zero.
# Return the number of cards left in the shoe.
def shoe_size(self):
return len(self.cards)
# Remove the card from the front of the shoe. Return it as value.
def draw_one_card(self):
# For safety, ensure that there is at least 1 card in the shoe at all times.
if self.shoe_size() <= 1:
self.replenish()
choice = self.cards.pop(0) # Take the first card off the front of the shoe.
return choice # Return the chosen card.
class Hand:
def __init__(self, name, stake, ancestors):
self.name = name # User friendly name for the hand of cards.
self.stake = stake
self.ancestors = ancestors # How many ancestors does this hand have?
self.cards = [] # The hand starts with an empty list of cards.
self.value = 0 # Current value of the hand.
self.busted = False # Is the hand busted?
self.blackjack = False
# Print a summary of the hand.
def print(self):
print(self.name, end=":")
for c in self.cards:
(card, _, _) = c
print("", card, end="")
print(", Value=%d" % self.value, end="")
if self.busted:
print(", Busted", end="")
else:
print(", Not-busted", end="")
if self.blackjack:
print(", Blackjack", end="")
else:
print(", No Blackjack", end="")
print(", Stake=£%d" % self.stake, end="")
print()
# Calculate the points value of a hand. If the hand is busted, it sets the busted attribute to True.
# For hands containing 1 or more aces, there are multiple possible value. This method returns the highest
# non-busted value possible. If the hand is busted, it returns the lowest points value possible.
def calculate_value(self):
possible_values = [0] # List of all possible values of this hand.
for c in self.cards:
(_, value_list, _) = c
new_possible_values = []
for p in possible_values:
for v in value_list:
new_possible_values.append(p + v)
possible_values = new_possible_values
self.value = 0
self.busted = True
for p in possible_values:
if p > self.value and p <=21:
self.value = p # We found a new higher, non-busted value.
self.busted = False
# If we busted, it is still nice to set the value to the lowest possible value the cards in the hand can make.
if self.busted:
self.value = min(possible_values)
# This method should only be called for the 1st hands (player or dealer) of each round. Hands created from
# splits cannot be blackjacks.
def check_blackjack(self):
if self.value == 21 and self.ancestors == 0: # Check no ancestors. Hands with ancestors are not 1st hands.
self.blackjack = True
# Add the parm card to this hand.
def receive_card(self, card):
self.cards.append(card) # Append the parm card to the list of cards in this hand.
self.calculate_value() # Recalculate the value of this hand.
class Table:
def __init__(self, decks, penetration):
self.shoe = Shoe() # Create an empty card shoe.
# Overwrite the shoe's defaults for decks and penetration with the table's parms.
self.shoe.decks = decks
self.shoe.penetration = penetration
self.shoe.replenish() # ... fill the shoe with normal, randomised cards.
# Normal stake is £4. Chosen as it can be split into 4 hands while remaining an integer.
self.betting_unit = 4
self.blackjack_value = 3 / 2 # Eg. £4 bet wins £6 for a blackjack (and stake returned too).
self.rounds_played = 0
self.hands_played_by_player = 0
self.total_staked = 0
self.amount_won_or_lost = 0 # +ve number means the player is in profit, -ve means loss.
self.dealer = None # The dealer's current hand on the table.
self.player_hands = [] # List of player hands currently on the table.
# Update running totals to reflect money staked by player.
def invest(self, stake):
self.total_staked += stake
self.amount_won_or_lost -= stake
# Double down the stake on this hand.
def double_down(self, hand):
self.invest(hand.stake)
hand.stake += hand.stake
# Add parm winnings to player running total or money won (or lost).
def win(self, winnings):
self.amount_won_or_lost += winnings
# Deal one card from the front of the shoe to one of the hands.
def deal_one_card(self, hand):
card = self.shoe.draw_one_card() # Draw a card from the shoe,
hand.receive_card(card) # ... and add it to the parm hand.
return card # Return the card to caller.
# When a card is dealt face up, it's value is visible, so the card count can be adjusted.
def deal_one_card_face_up(self, hand):
card = self.deal_one_card(hand)
current_shoe_size = self.shoe.shoe_size()
# This card has been dealt face up, so adjust the card count.
self.shoe.card_count.adjust_count(card, current_shoe_size, self.betting_unit)
def print_table_status(self):
print("Rounds played=%d, Total staked=£%d, Amount won or lost=£%d"
% (self.rounds_played, self.total_staked, self.amount_won_or_lost))
# This strategy is to Stand on 17 to 21, and to Hit on anything less than 17. In most casinos, this is also the
# strategy that the Dealer is required to follow.
def dealer_stategy(self, hand, dealer_up_card):
while hand.value < 17 and not hand.busted:
self.deal_one_card_face_up(hand)
# This section of Basic Strategy looks at the dealer's up card to make a more nuanced decision about whether
# to Hit or Stand.
def basic_strategy_section_1(self, hand, dealer_up_card):
(_, _, dealer_card_value) = dealer_up_card
finished = False
while not finished:
if hand.value == 12 and dealer_card_value in ["2", "3"] \
or (hand.value in [12, 13, 14, 15, 16] and dealer_card_value in ["7", "8", "9", "10", "A"]) \
or hand.value <= 11:
self.deal_one_card_face_up(hand) # "Hit"
else:
finished = True # "Stand"
# The section of Basic Strategy introduces rules for when to Double Down.
def basic_strategy_section_2(self, hand, dealer_up_card):
(_, _, dealer_card_value) = dealer_up_card
# It is only possible to Double Down with first 2 cards of the hand on table.
double_down = False
if hand.value == 11:
if dealer_card_value != "A":
double_down = True
elif hand.value == 10:
if not dealer_card_value in ["10", "A"]:
double_down = True
elif hand.value == 9 and dealer_card_value in ["3", "4", "5", "6"]:
double_down = True
if double_down: # "Double Down"
self.double_down(hand)
self.deal_one_card_face_up(hand) # One more card only, after Double Down.
# If we are not doubling down, then play Basic Strategy Section 1.
else:
self.basic_strategy_section_1(hand, dealer_up_card)
# This section of Basic Strategy introduces rules for player hands that include an ace.
def basic_strategy_section_3(self, hand, dealer_up_card):
other_card = ""
decision = ""
# Work out the values of both of the player's cards.
(_, _, player_card_1_value) = hand.cards[0]
(_, _, player_card_2_value) = hand.cards[1]
if player_card_1_value == "A":
other_card = player_card_2_value
elif player_card_2_value == "A":
other_card = player_card_1_value
# Are either of the player's cards an ace?
if other_card != "":
(_, _, dealer_card_value) = dealer_up_card
if other_card in ["8", "9", "10"]:
decision = "Stand"
elif other_card == "7":
if dealer_card_value in ["2", "7", "8"]:
decision = "Stand"
elif dealer_card_value in ["9", "10", "A"]:
decision = "Hit"
else:
decision = "Double Down"
elif other_card == "6":
if dealer_card_value in ["3", "4", "5", "6"]:
decision = "Double Down"
else:
decision = "Hit"
elif other_card in ["4", "5"]:
if dealer_card_value in ["4", "5", "6"]:
decision = "Double Down"
else:
decision = "Hit"
elif other_card in ["2", "3"]:
if dealer_card_value in ["5", "6"]:
decision = "Double Down"
else:
decision = "Hit"
if other_card == "": # If neither card is an ace,
self.basic_strategy_section_2(hand, dealer_up_card) # ... continue with Section 2 strategy.
elif decision == "Double Down":
self.double_down(hand) # Double stake.
self.deal_one_card_face_up(hand) # One more card only, after Double Down.
elif decision == "Hit":
self.deal_one_card_face_up(hand) # "Hit",
self.basic_strategy_section_2(hand, dealer_up_card) # ... and then continue with Section 2 strategy.
elif decision != "Stand": # "Stand" means do nothing, but otherwise,
self.basic_strategy_section_2(hand, dealer_up_card) # ... continue with Section 2 strategy.
# This section of Basic Strategy introduces rules for player hands which with matching card values, which may
# be Split.
def basic_strategy_section_4(self, hand, dealer_up_card):
# Work out the values of both of the player's cards.
(_, _, player_card_1_value) = hand.cards[0]
(_, _, player_card_2_value) = hand.cards[1]
# Rule is that 1st hand can only be split twice. Ie. no more than 4 hands for 1 player in a single game.
# And a hand can only be split if both cards are equal value.
if hand.ancestors <= 1 and player_card_1_value == player_card_2_value:
(_, _, dealer_card_value) = dealer_up_card
decision_map = \
{("A", "2"): "SP", ("A", "3"): "SP", ("A", "4"): "SP", ("A", "5"): "SP", ("A", "6"): "SP", ("A", "7"): "SP", ("A", "8"): "SP", ("A", "9"): "SP", ("A", "10"): "SP", ("A", "A"): "SP",
("10", "2"): "S", ("10", "3"): "S", ("10", "4"): "S", ("10", "5"): "S", ("10", "6"): "S", ("10", "7"): "S", ("10", "8"): "S", ("10", "9"): "S", ("10", "10"): "S", ("10", "A"): "S",
("9", "2"): "SP", ("9", "3"): "SP", ("9", "4"): "SP", ("9", "5"): "SP", ("9", "6"): "SP", ("9", "7"): "S", ("9", "8"): "SP", ("9", "9"): "SP", ("9", "10"): "S", ("9", "A"): "S",
("8", "2"): "SP", ("8", "3"): "SP", ("8", "4"): "SP", ("8", "5"): "SP", ("8", "6"): "SP", ("8", "7"): "SP", ("8", "8"): "SP", ("8", "9"): "SP", ("8", "10"): "SP", ("8", "A"): "SP",
("7", "2"): "SP", ("7", "3"): "SP", ("7", "4"): "SP", ("7", "5"): "SP", ("7", "6"): "SP", ("7", "7"): "SP", ("7", "8"): "H", ("7", "9"): "H", ("7", "10"): "H", ("7", "A"): "H",
("6", "2"): "SP", ("6", "3"): "SP", ("6", "4"): "SP", ("6", "5"): "SP", ("6", "6"): "SP", ("6", "7"): "H", ("6", "8"): "H", ("6", "9"): "H", ("6", "10"): "H", ("6", "A"): "H",
("5", "2"): "D", ("5", "3"): "D", ("5", "4"): "D", ("5", "5"): "D", ("5", "6"): "D", ("5", "7"): "D", ("5", "8"): "D", ("5", "9"): "D", ("5", "10"): "H", ("5", "A"): "H",
("4", "2"): "H", ("4", "3"): "H", ("4", "4"): "H", ("4", "5"): "SP", ("4", "6"): "SP", ("4", "7"): "H", ("4", "8"): "H", ("4", "9"): "H", ("4", "10"): "H", ("4", "A"): "H",
("3", "2"): "SP", ("3", "3"): "SP", ("3", "4"): "SP", ("3", "5"): "SP", ("3", "6"): "SP", ("3", "7"): "SP", ("3", "8"): "H", ("3", "9"): "H", ("3", "10"): "H", ("3", "A"): "H",
("2", "2"): "SP", ("2", "3"): "SP", ("2", "4"): "SP", ("2", "5"): "SP", ("2", "6"): "SP", ("2", "7"): "SP", ("2", "8"): "H", ("2", "9"): "H", ("2", "10"): "H", ("2", "A"): "H"}
decision = decision_map.get((player_card_1_value, dealer_card_value))
if decision == "SP": # "Split"
hand.stake = hand.stake / 2 # Stake is split between parent and child hands.
# Add the new child hand to the list of player's hands that are on the table.
self.player_hands.append(Hand("Child of " + hand.name,
hand.stake, # New hand has same newly halved stake as parent.
hand.ancestors + 1)) # It has 1 more ancestor than its parent.
new_hand_index = len(self.player_hands) -1
self.hands_played_by_player += 1
# Move 2nd card from parent hand to child hand.
second_card = hand.cards.pop(1)
self.player_hands[new_hand_index].receive_card(second_card)
# Deal a card to parent, so that it has 2 cards again.
self.deal_one_card_face_up(hand)
# The parent hand is now a child too.
hand.ancestors += 1
# Now apply this strategy to the new version of the parent hand.
self.basic_strategy_section_4(hand, dealer_up_card)
# Now work on the child hand.
self.deal_one_card_face_up(self.player_hands[new_hand_index])
# Now apply this strategy to the child hand.
self.basic_strategy_section_4(self.player_hands[new_hand_index], dealer_up_card)
elif decision == "D": # "Double Down"
self.double_down(hand) # Double stake.
self.deal_one_card_face_up(hand) # One more card only, after Double Down.
elif decision == "H": # "Hit"
self.deal_one_card_face_up(hand) # Deal one card to the hand.
self.basic_strategy_section_3(hand, dealer_up_card) # ... and then continue with Section 3 strategy.
elif decision != "S": # "Stand" means do nothing, but otherwise,
self.basic_strategy_section_3(hand, dealer_up_card) # ... continue with Section 3 strategy.
else:
self.basic_strategy_section_3(hand, dealer_up_card)
def play_one_round(self, strategy_name):
self.rounds_played += 1 # Increment number of rounds played on this table.
# Create a hand of cards for the dealer. Dealer has no money staked.
self.dealer = Hand("Dealer Hand",
0, # Dealer has no money staked.
0) # This hand has no ancestors.
if verbose:
self.shoe.card_count.print() # Print the status of the card counting.
# If we're doing card counting, then bet according to the calculation of the card counting strategy.
if strategy_name == "Hi-Lo Card Count":
this_stake = self.shoe.card_count.bet_size
else:
this_stake = self.betting_unit
self.invest(this_stake)
# Create a first hand of cards for the player.
self.player_hands.append(Hand("Player Hand",
this_stake,
0)) # The player's first hand has no ancestors.
self.hands_played_by_player += 1
# Deal first 4 cards in classic order (Player, Dealer, Player, Dealer).
self.deal_one_card_face_up(self.player_hands[0]) # Face up.
self.deal_one_card_face_up(self.dealer) # Face up.
self.deal_one_card_face_up(self.player_hands[0]) # Face up.
self.deal_one_card(self.dealer) # Not face up.
# Check both Player and Dealer's hands for Blackjack on original 2 cards only.
self.dealer.check_blackjack()
self.player_hands[0].check_blackjack()
if self.player_hands[0].blackjack: # Player has a Blackjack.
if self.dealer.blackjack: # Dealer also has a Blackjack,
self.win(self.player_hands[0].stake) # ... so player wins his stake back.
else: # Player has Blackjack, and dealer doesn't have one,
# ... so he wins his stake back, plus his stake multiplied by Blackjack odds.
self.win(self.player_hands[0].stake + self.blackjack_value * self.player_hands[0].stake)
# No Blackjack for the player, so proceed to a playing strategy.
else:
# The Dealer's up card, is the first card in his hand. Some strategies vary their decisions based upon it.
dealer_up_card = self.dealer.cards[0]
# Convert the parm strategy string into a strategy function.
# Note that card counting is based on Basic Strategy.
strategy_map = {"Dealer": self.dealer_stategy,
"Basic Strategy Section 1": self.basic_strategy_section_1,
"Basic Strategy Section 2": self.basic_strategy_section_2,
"Basic Strategy Section 3": self.basic_strategy_section_3,
"Basic Strategy Section 4": self.basic_strategy_section_4,
"Hi-Lo Card Count": self.basic_strategy_section_4}
strategy = strategy_map.get(strategy_name)
strategy(self.player_hands[0], dealer_up_card)
# Does the player have any non-busted hands?
all_player_hands_busted = True
for ph in self.player_hands:
if ph.busted == False:
all_player_hands_busted = False
# If player has executed his strategy without busting all of his hands,
# and the dealer doesn't have a Blackjack...
# ... then it is the dealer's turn to play.
if not all_player_hands_busted and not self.dealer.blackjack:
# First turn over the dealer's second card (index=1), and adjust the card count.
self.shoe.card_count.adjust_count(self.dealer.cards[1], self.shoe.shoe_size(), self.betting_unit)
self.dealer_stategy(self.dealer, dealer_up_card)
# Compare each of the player's hands with the dealer's hand.
for ph in self.player_hands:
if not ph.busted: # Only interested in non-busted player hands.
# If Dealer has busted, or Player has higher score than Dealer, then pleyer wins.
if self.dealer.busted or ph.value > self.dealer.value:
self.win(2 * ph.stake) # Win 1*stake as winnings, plus get stake back.
# Dealer and Player have matching scores, so no winner... but Player does get his stake back.
elif ph.value == self.dealer.value:
self.win(ph.stake)
if verbose:
for ph in self.player_hands:
ph.print() # Print all of the dealer's hands.
self.dealer.print() # Print the dealer's hand.
self.shoe.print() # Print status of the shoe.
self.print_table_status() # Print the status of the table.
print()
# Game is over, so Player & Dealer dispose of their hands of cards.
del self.dealer
self.player_hands = []
# Check if the shoe needs to be replenished.
self.shoe.replenish()
verbose = False