From a075de78b14da01bc0ba539f9e3185c8246b655b Mon Sep 17 00:00:00 2001 From: Thomas Chen Date: Thu, 4 Jan 2018 17:38:22 -0800 Subject: [PATCH 1/4] Dealer should not be drawing cards if the player busts. Under normal deal sequence, dealer will have 2 cards even when player busts, so draw only one card if player busts. --- BlackJack.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/BlackJack.py b/BlackJack.py index 5742414..b0aa075 100644 --- a/BlackJack.py +++ b/BlackJack.py @@ -434,7 +434,10 @@ def play_round(self): # print "Player Hand: %s\n" % self.player.hands[0] self.player.play(self.shoe) - self.dealer.play(self.shoe) + if not self.player.hands[0].busted(): + self.dealer.play(self.shoe) + else: + dealer_hand.add_card(self.shoe.deal()) # print "" From 010ca7cb8da4810c83482aac94bad78fab0434d5 Mon Sep 17 00:00:00 2001 From: Thomas Chen Date: Sat, 6 Jan 2018 17:20:44 -0800 Subject: [PATCH 2/4] This adds an interactive mode and allows the user to play the game. A log records player actions and the data can later be used to create a stochastic simulation of player behavior given current skill level. --- BlackJack.py | 93 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/BlackJack.py b/BlackJack.py index b0aa075..e87ae08 100644 --- a/BlackJack.py +++ b/BlackJack.py @@ -5,6 +5,7 @@ import scipy.stats as stats import pylab as pl import matplotlib.pyplot as plt +import pandas as pd from importer.StrategyImporter import StrategyImporter @@ -16,7 +17,7 @@ DECK_SIZE = 52.0 CARDS = {"Ace": 11, "Two": 2, "Three": 3, "Four": 4, "Five": 5, "Six": 6, "Seven": 7, "Eight": 8, "Nine": 9, "Ten": 10, "Jack": 10, "Queen": 10, "King": 10} -BASIC_OMEGA_II = {"Ace": 0, "Two": 1, "Three": 1, "Four": 2, "Five": 2, "Six": 2, "Seven": 1, "Eight": 0, "Nine": -1, "Ten": -2, "Jack": -2, "Queen": -2, "King": -2} +BASIC_OMEGA_II = {"Ace": 0, "Two": 1, "Three": 1, "Four": 2, "Five": 2, "Six": 2, "Seven": 1, "Eight": 0, "Nine":-1, "Ten":-2, "Jack":-2, "Queen":-2, "King":-2} BLACKJACK_RULES = { 'triple7': False, # Count 3x7 as a blackjack @@ -38,6 +39,10 @@ def __init__(self, name, value): def __str__(self): return "%s" % self.name + @property + def count(self): + return BASIC_OMEGA_II[self.name] + class Shoe(object): """ @@ -134,7 +139,7 @@ def __init__(self, cards): def __str__(self): h = "" for c in self.cards: - h += "%s " % c + h += "%s[%s] " % (c, c.count) return h @property @@ -243,6 +248,26 @@ def length(self): return len(self.cards) +class Log(object): + """ + Represents a history of hands and associated actions. + """ + def __init__(self): + self.hands = None + + def add_hand(self, action, hand, dealer_hand, shoe): + d = {'hand': [hand.value], 'soft': [hand.soft()], + 'splitable': [hand.splitable()], + 'dealer': [dealer_hand.cards[0].value], + 'truecount': [shoe.truecount()], 'action': [action.upper()] + } + if self.hands is None: + self.hands = pd.DataFrame(data=d) + else: + self.hands = self.hands.append(pd.DataFrame(data=d)) + print(self.hands) + + class Player(object): """ Represent a player @@ -250,6 +275,8 @@ class Player(object): def __init__(self, hand=None, dealer_hand=None): self.hands = [hand] self.dealer_hand = dealer_hand + self.autoplay = True + self.history = Log() def set_hands(self, new_hand, new_dealer_hand): self.hands = [new_hand] @@ -267,14 +294,22 @@ def play_hand(self, hand, shoe): self.hit(hand, shoe) while not hand.busted() and not hand.blackjack(): - if hand.soft(): - flag = SOFT_STRATEGY[hand.value][self.dealer_hand.cards[0].name] - elif hand.splitable(): - flag = PAIR_STRATEGY[hand.value][self.dealer_hand.cards[0].name] + if self.autoplay: + if hand.soft(): + flag = SOFT_STRATEGY[hand.value][self.dealer_hand.cards[0].name] + elif hand.splitable(): + flag = PAIR_STRATEGY[hand.value][self.dealer_hand.cards[0].name] + else: + flag = HARD_STRATEGY[hand.value][self.dealer_hand.cards[0].name] else: - flag = HARD_STRATEGY[hand.value][self.dealer_hand.cards[0].name] - - if flag == 'D': + print("Dealer Hand: %s (%d)" % (self.dealer_hand, self.dealer_hand.value)) + print("Player Hand: %s (%d)" % (self.hands[0], self.hands[0].value)) + print("Count=%s, Penetration=%s\n" % ("{0:.2f}".format(shoe.count), "{0:.2f}".format(shoe.shoe_penetration()))) + flag = input("Action (H=Hit, S=Stand, D=Double, P=Split, Sr=Surrender, Q=Quit): ") + if flag != 'Q': + self.history.add_hand(flag, hand, self.dealer_hand, shoe) + + if flag.upper() == 'D': if hand.length() == 2: # print "Double Down" hand.doubled = True @@ -283,7 +318,7 @@ def play_hand(self, hand, shoe): else: flag = 'H' - if flag == 'Sr': + if flag.upper() == 'SR': if hand.length() == 2: # print "Surrender" hand.surrender = True @@ -291,15 +326,18 @@ def play_hand(self, hand, shoe): else: flag = 'H' - if flag == 'H': + if flag.upper() == 'H': self.hit(hand, shoe) - if flag == 'P': + if flag.upper() == 'P': self.split(hand, shoe) - if flag == 'S': + if flag.upper() == 'S': break + if flag.upper() == 'Q': + exit() + def hit(self, hand, shoe): c = shoe.deal() hand.add_card(c) @@ -356,7 +394,7 @@ def add_a_statistical_card(self, stat_card): for p in self.tree[-1] : for v in stat_card : new_value = v + p - proba = self.tree[-1][p]*stat_card[v] + proba = self.tree[-1][p] * stat_card[v] if (new_value > 21) : # All busted values are 22 new_value = 22 @@ -421,10 +459,18 @@ def get_hand_winnings(self, hand): return win, bet def play_round(self): - if self.shoe.truecount() > 6: - self.stake = BET_SPREAD + if self.player.autoplay: + if self.shoe.truecount() > 6: + self.stake = BET_SPREAD + else: + self.stake = 1.0 else: - self.stake = 1.0 + raw_stake = input("Bet (%s): " % self.stake) + if raw_stake != "": + try: + self.stake = float(raw_stake) + except ValueError: + print("Invalid bet, using default.") player_hand = Hand([self.shoe.deal(), self.shoe.deal()]) dealer_hand = Hand([self.shoe.deal()]) @@ -464,11 +510,18 @@ def get_bet(self): bets = [] countings = [] nb_hands = 0 + GAMES = int(input("How many games? ")) for g in range(GAMES): game = Game() + autoplay = input("Autoplay? (y/n): ") + if autoplay == 'n': + game.player.autoplay = False while not game.shoe.reshuffle: # print '%s GAME no. %d %s' % (20 * '#', i + 1, 20 * '#') game.play_round() + print("Dealer Hand: %s (%d)" % (game.dealer.hand, game.dealer.hand.value)) + print("Player Hand: %s (%d)\n" % (game.player.hands[0], game.player.hands[0].value)) + nb_hands += 1 moneys.append(game.get_money()) @@ -484,9 +537,9 @@ def get_bet(self): for value in bets: total_bet += value - print "\n%d hands overall, %0.2f hands per game on average" % (nb_hands, float(nb_hands) / GAMES) - print "%0.2f total bet" % total_bet - print("Overall winnings: {} (edge = {} %)".format("{0:.2f}".format(sume), "{0:.3f}".format(100.0*sume/total_bet))) + print("\n%d hands overall, %0.2f hands per game on average" % (nb_hands, float(nb_hands) / GAMES)) + print("%0.2f total bet" % total_bet) + print("Overall winnings: {} (edge = {} %)".format("{0:.2f}".format(sume), "{0:.3f}".format(100.0 * sume / total_bet))) moneys = sorted(moneys) fit = stats.norm.pdf(moneys, np.mean(moneys), np.std(moneys)) # this is a fitting indeed From 2a3f2b7809c79e44a619dd4839c01b943b9a8d77 Mon Sep 17 00:00:00 2001 From: Thomas Chen Date: Sat, 6 Jan 2018 18:21:16 -0800 Subject: [PATCH 3/4] Just fixing style. --- BlackJack.py | 101 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 36 deletions(-) diff --git a/BlackJack.py b/BlackJack.py index e87ae08..2b39c8c 100644 --- a/BlackJack.py +++ b/BlackJack.py @@ -16,8 +16,15 @@ BET_SPREAD = 20.0 DECK_SIZE = 52.0 -CARDS = {"Ace": 11, "Two": 2, "Three": 3, "Four": 4, "Five": 5, "Six": 6, "Seven": 7, "Eight": 8, "Nine": 9, "Ten": 10, "Jack": 10, "Queen": 10, "King": 10} -BASIC_OMEGA_II = {"Ace": 0, "Two": 1, "Three": 1, "Four": 2, "Five": 2, "Six": 2, "Seven": 1, "Eight": 0, "Nine":-1, "Ten":-2, "Jack":-2, "Queen":-2, "King":-2} +CARDS = { + "Ace": 11, "Two": 2, "Three": 3, "Four": 4, "Five": 5, "Six": 6, + "Seven": 7, "Eight": 8, "Nine": 9, "Ten": 10, "Jack": 10, "Queen": 10, + "King": 10 + } +BASIC_OMEGA_II = { + "Ace": 0, "Two": 1, "Three": 1, "Four": 2, "Five": 2, "Six": 2, "Seven": 1, + "Eight": 0, "Nine": -1, "Ten": -2, "Jack": -2, "Queen": -2, "King": -2 + } BLACKJACK_RULES = { 'triple7': False, # Count 3x7 as a blackjack @@ -81,16 +88,17 @@ def init_cards(self): def init_count(self): """ - Keep track of the number of occurrences for each card in the shoe in the course over the game. ideal_count - is a dictionary containing (card name - number of occurrences in shoe) pairs + Keep track of the number of occurrences for each card in the shoe in + the course over the game. ideal_count is a dictionary containing (card + name - number of occurrences in shoe) pairs """ for card in CARDS: self.ideal_count[card] = 4 * SHOE_SIZE def deal(self): """ - Returns: The next card off the shoe. If the shoe penetration is reached, - the shoe gets reshuffled. + Returns: The next card off the shoe. If the shoe penetration is + reached, the shoe gets reshuffled. """ if self.shoe_penetration() < SHOE_PENETRATION: self.reshuffle = True @@ -117,7 +125,8 @@ def truecount(self): def shoe_penetration(self): """ - Returns: Ratio of cards that are still in the shoe to all initial cards. + Returns: Ratio of cards that are still in the shoe to all initial + cards. """ return len(self.cards) / (DECK_SIZE * self.decks) @@ -145,7 +154,8 @@ def __str__(self): @property def value(self): """ - Returns: The current value of the hand (aces are either counted as 1 or 11). + Returns: The current value of the hand (aces are either counted as 1 or + 11). """ self._value = 0 for c in self.cards: @@ -185,7 +195,8 @@ def aces_soft(self): def soft(self): """ - Determines whether the current hand is soft (soft means that it consists of aces valued at 11). + Determines whether the current hand is soft (soft means that it + consists of aces valued at 11). """ if self.aces_soft > 0: return True @@ -203,10 +214,12 @@ def splitable(self): def blackjack(self): """ - Check a hand for a blackjack, taking the defined BLACKJACK_RULES into account. + Check a hand for a blackjack, taking the defined BLACKJACK_RULES into + account. """ if not self.splithand and self.value == 21: - if all(c.value == 7 for c in self.cards) and BLACKJACK_RULES['triple7']: + if (all(c.value == 7 for c in self.cards) and + BLACKJACK_RULES['triple7']): return True elif self.length() == 2: return True @@ -296,16 +309,24 @@ def play_hand(self, hand, shoe): while not hand.busted() and not hand.blackjack(): if self.autoplay: if hand.soft(): - flag = SOFT_STRATEGY[hand.value][self.dealer_hand.cards[0].name] + flag = SOFT_STRATEGY[hand.value][ + self.dealer_hand.cards[0].name] elif hand.splitable(): - flag = PAIR_STRATEGY[hand.value][self.dealer_hand.cards[0].name] + flag = PAIR_STRATEGY[hand.value][ + self.dealer_hand.cards[0].name] else: - flag = HARD_STRATEGY[hand.value][self.dealer_hand.cards[0].name] + flag = HARD_STRATEGY[hand.value][ + self.dealer_hand.cards[0].name] else: - print("Dealer Hand: %s (%d)" % (self.dealer_hand, self.dealer_hand.value)) - print("Player Hand: %s (%d)" % (self.hands[0], self.hands[0].value)) - print("Count=%s, Penetration=%s\n" % ("{0:.2f}".format(shoe.count), "{0:.2f}".format(shoe.shoe_penetration()))) - flag = input("Action (H=Hit, S=Stand, D=Double, P=Split, Sr=Surrender, Q=Quit): ") + print("Dealer Hand: %s (%d)" % + (self.dealer_hand, self.dealer_hand.value)) + print("Player Hand: %s (%d)" % + (self.hands[0], self.hands[0].value)) + print("Count=%s, Penetration=%s\n" % + ("{0:.2f}".format(shoe.count), + "{0:.2f}".format(shoe.shoe_penetration()))) + flag = input("Action (H=Hit, S=Stand, D=Double, P=Split, " + "Sr=Surrender, Q=Quit): ") if flag != 'Q': self.history.add_hand(flag, hand, self.dealer_hand, shoe) @@ -368,13 +389,16 @@ def hit(self, shoe): self.hand.add_card(c) # print "Dealer hitted: %s" %c - # Returns an array of 6 numbers representing the probability that the final score of the dealer is + # Returns an array of 6 numbers representing the probability that the final + # score of the dealer is # [17, 18, 19, 20, 21, Busted] ''' # TODO Differentiate 21 and BJ # TODO make an actual tree, this is false AF - def get_probabilities(self) : + def get_probabilities(self): start_value = self.hand.value - # We'll draw 5 cards no matter what an count how often we got 17, 18, 19, 20, 21, Busted + # We'll draw 5 cards no matter what an count how often we got 17, 18, + # 19, 20, 21, Busted + class Tree(object): """ @@ -391,16 +415,16 @@ def __init__(self, start=[]): def add_a_statistical_card(self, stat_card): # New set of leaves in the tree leaves = [] - for p in self.tree[-1] : - for v in stat_card : + for p in self.tree[-1]: + for v in stat_card: new_value = v + p proba = self.tree[-1][p] * stat_card[v] - if (new_value > 21) : + if (new_value > 21): # All busted values are 22 new_value = 22 - if (new_value in leaves) : + if (new_value in leaves): leaves[new_value] = leaves[new_value] + proba - else : + else: leaves[new_value] = proba @@ -491,9 +515,6 @@ def play_round(self): win, bet = self.get_hand_winnings(hand) self.money += win self.bet += bet - # print "Player Hand: %s %s (Value: %d, Busted: %r, BlackJack: %r, Splithand: %r, Soft: %r, Surrender: %r, Doubled: %r)" % (hand, status, hand.value, hand.busted(), hand.blackjack(), hand.splithand, hand.soft(), hand.surrender, hand.doubled) - - # print "Dealer Hand: %s (%d)" % (self.dealer.hand, self.dealer.hand.value) def get_money(self): return self.money @@ -504,7 +525,8 @@ def get_bet(self): if __name__ == "__main__": importer = StrategyImporter(sys.argv[1]) - HARD_STRATEGY, SOFT_STRATEGY, PAIR_STRATEGY = importer.import_player_strategy() + HARD_STRATEGY, SOFT_STRATEGY, PAIR_STRATEGY = ( + importer.import_player_strategy()) moneys = [] bets = [] @@ -519,8 +541,10 @@ def get_bet(self): while not game.shoe.reshuffle: # print '%s GAME no. %d %s' % (20 * '#', i + 1, 20 * '#') game.play_round() - print("Dealer Hand: %s (%d)" % (game.dealer.hand, game.dealer.hand.value)) - print("Player Hand: %s (%d)\n" % (game.player.hands[0], game.player.hands[0].value)) + print("Dealer Hand: %s (%d)" % + (game.dealer.hand, game.dealer.hand.value)) + print("Player Hand: %s (%d)\n" % + (game.player.hands[0], game.player.hands[0].value)) nb_hands += 1 @@ -528,7 +552,9 @@ def get_bet(self): bets.append(game.get_bet()) countings += game.shoe.count_history - print("WIN for Game no. %d: %s (%s bet)" % (g + 1, "{0:.2f}".format(game.get_money()), "{0:.2f}".format(game.get_bet()))) + print("WIN for Game no. %d: %s (%s bet)" % + (g + 1, "{0:.2f}".format(game.get_money()), + "{0:.2f}".format(game.get_bet()))) sume = 0.0 total_bet = 0.0 @@ -537,12 +563,15 @@ def get_bet(self): for value in bets: total_bet += value - print("\n%d hands overall, %0.2f hands per game on average" % (nb_hands, float(nb_hands) / GAMES)) + print("\n%d hands overall, %0.2f hands per game on average" % + (nb_hands, float(nb_hands) / GAMES)) print("%0.2f total bet" % total_bet) - print("Overall winnings: {} (edge = {} %)".format("{0:.2f}".format(sume), "{0:.3f}".format(100.0 * sume / total_bet))) + print("Overall winnings: {} (edge = {} %)".format( + "{0:.2f}".format(sume), "{0:.3f}".format(100.0 * sume / total_bet)) + ) moneys = sorted(moneys) - fit = stats.norm.pdf(moneys, np.mean(moneys), np.std(moneys)) # this is a fitting indeed + fit = stats.norm.pdf(moneys, np.mean(moneys), np.std(moneys)) pl.plot(moneys, fit, '-o') pl.hist(moneys, normed=True) pl.show() From d75af8ec87d4d20e3f18ff9efe2041822fb02533 Mon Sep 17 00:00:00 2001 From: Thomas Chen Date: Sat, 6 Jan 2018 19:03:14 -0800 Subject: [PATCH 4/4] Serialize and deserialize the player history. --- BlackJack.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/BlackJack.py b/BlackJack.py index 2b39c8c..6a9e6a5 100644 --- a/BlackJack.py +++ b/BlackJack.py @@ -1,4 +1,5 @@ import sys +import os from random import shuffle import numpy as np @@ -10,6 +11,7 @@ from importer.StrategyImporter import StrategyImporter +scriptDirectory = os.path.dirname(os.path.realpath(__file__)) GAMES = 20000 SHOE_SIZE = 6 SHOE_PENETRATION = 0.25 @@ -205,7 +207,7 @@ def soft(self): def splitable(self): """ - Determines if the current hand can be splitted. + Determines if the current hand can be split. """ if self.length() == 2 and self.cards[0].name == self.cards[1].name: return True @@ -266,7 +268,13 @@ class Log(object): Represents a history of hands and associated actions. """ def __init__(self): - self.hands = None + try: + self.hands = pd.read_pickle(scriptDirectory+'/player_history') + except FileNotFoundError: + self.hands = None + + def __str__(self): + print(self.hands) def add_hand(self, action, hand, dealer_hand, shoe): d = {'hand': [hand.value], 'soft': [hand.soft()], @@ -278,7 +286,9 @@ def add_hand(self, action, hand, dealer_hand, shoe): self.hands = pd.DataFrame(data=d) else: self.hands = self.hands.append(pd.DataFrame(data=d)) - print(self.hands) + + def save(self): + self.hands.to_pickle(scriptDirectory+'/player_history') class Player(object): @@ -556,6 +566,8 @@ def get_bet(self): (g + 1, "{0:.2f}".format(game.get_money()), "{0:.2f}".format(game.get_bet()))) + if game.player.autoplay is False: + game.player.history.save() sume = 0.0 total_bet = 0.0 for value in moneys: