|
import argparse |
|
|
|
""" |
|
Blackjack Advisor for Dealer Hits on Soft 17 |
|
|
|
This will always respond with Hit, Stand, or Split. |
|
It may emit Surrender if it is most advisable and the move is legal. |
|
It may emit Double if it is most advisable and the move is legal |
|
|
|
Author: Kyle Bloom |
|
""" |
|
|
|
""" |
|
strategy_chart |
|
|
|
All values must have either 1 or 2 values. |
|
If the first value is H, S, or P, it is ok if it is alone, for they are always legal moves. |
|
If the first value is X or D, they must be accompanied with either H, S, or P, for X or D may not be legal moves. |
|
""" |
|
strategy_chart = { |
|
'9': {'2': 'H', '3': 'DH', '4': 'DH', '5': 'DH', '6': 'DH', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'10': {'2': 'DH', '3': 'DH', '4': 'DH', '5': 'DH', '6': 'DH', '7': 'DH', '8': 'DH', '9': 'DH', '10': 'H', 'A': 'H'}, |
|
'11': {'2': 'DH', '3': 'DH', '4': 'DH', '5': 'DH', '6': 'DH', '7': 'DH', '8': 'DH', '9': 'DH', '10': 'DH', 'A': 'DH'}, |
|
'12': {'2': 'H', '3': 'H', '4': 'S', '5': 'S', '6': 'S', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'13': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'14': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'15': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'H', '8': 'H', '9': 'H', '10': 'XH', 'A': 'XH'}, |
|
'16': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'H', '8': 'H', '9': 'XH', '10': 'XH', 'A': 'XH'}, |
|
'17': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'S', '8': 'S', '9': 'S', '10': 'S', 'A': 'XS'}, |
|
'2,A': {'2': 'H', '3': 'H', '4': 'H', '5': 'DH', '6': 'DH', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'3,A': {'2': 'H', '3': 'H', '4': 'H', '5': 'DH', '6': 'DH', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'4,A': {'2': 'H', '3': 'H', '4': 'DH', '5': 'DH', '6': 'DH', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'5,A': {'2': 'H', '3': 'H', '4': 'DH', '5': 'DH', '6': 'DH', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'6,A': {'2': 'H', '3': 'DH', '4': 'DH', '5': 'DH', '6': 'DH', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'7,A': {'2': 'S', '3': 'DS', '4': 'DS', '5': 'DS', '6': 'DS', '7': 'S', '8': 'S', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'8,A': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'S', '8': 'S', '9': 'S', '10': 'S', 'A': 'S'}, |
|
'9,A': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'S', '8': 'S', '9': 'S', '10': 'S', 'A': 'S'}, |
|
'10,A': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'S', '8': 'S', '9': 'S', '10': 'S', 'A': 'S'}, |
|
'2,2': {'2': 'P', '3': 'P', '4': 'P', '5': 'P', '6': 'P', '7': 'P', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'3,3': {'2': 'P', '3': 'P', '4': 'P', '5': 'P', '6': 'P', '7': 'P', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'4,4': {'2': 'H', '3': 'H', '4': 'H', '5': 'P', '6': 'P', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'5,5': {'2': 'DH', '3': 'DH', '4': 'DH', '5': 'DH', '6': 'DH', '7': 'DH', '8': 'DH', '9': 'DH', '10': 'H', 'A': 'H'}, |
|
'6,6': {'2': 'P', '3': 'P', '4': 'P', '5': 'P', '6': 'P', '7': 'H', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'7,7': {'2': 'P', '3': 'P', '4': 'P', '5': 'P', '6': 'P', '7': 'P', '8': 'H', '9': 'H', '10': 'H', 'A': 'H'}, |
|
'8,8': {'2': 'P', '3': 'P', '4': 'P', '5': 'P', '6': 'P', '7': 'P', '8': 'P', '9': 'P', '10': 'P', 'A': 'XP'}, |
|
'9,9': {'2': 'P', '3': 'P', '4': 'P', '5': 'P', '6': 'P', '7': 'S', '8': 'P', '9': 'P', '10': 'S', 'A': 'S'}, |
|
'10,10': {'2': 'S', '3': 'S', '4': 'S', '5': 'S', '6': 'S', '7': 'S', '8': 'S', '9': 'S', '10': 'S', 'A': 'S'}, |
|
'A,A': {'2': 'P', '3': 'P', '4': 'P', '5': 'P', '6': 'P', '7': 'P', '8': 'P', '9': 'P', '10': 'P', 'A': 'P'} |
|
} |
|
|
|
legend = { |
|
'H': 'Hit', |
|
'S': 'Stand', |
|
'P': 'Split', |
|
'D': 'Double', |
|
'X': 'Surrender' |
|
} |
|
|
|
|
|
def card(string): |
|
""" |
|
Validates whether the entry is a valid card |
|
|
|
Arguments: |
|
string: the value to be validated |
|
Returns: |
|
str equal to the value of the card |
|
Raises: |
|
argparse.ArgumentError if the card is not valid |
|
""" |
|
string = string.upper() # sets the string to upper case |
|
if string in ['K', 'Q', 'J']: # if the card is a [K]ing, [Q]ueen, or [J]ack |
|
string = '10' # sets string to 10 |
|
if string in ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10']: # checks if the card is a legal card |
|
return string # returns best representation of the card |
|
raise argparse.ArgumentError('Invalid Card') # raises error if the card is invalid |
|
|
|
|
|
def optimize_hand(hand): |
|
""" |
|
Optimizes hand for use with strategy_chart |
|
|
|
Example: |
|
['2', '5', 'A'] = ['7', 'A'] |
|
left hand side would normally advice Stay |
|
right hand side advices Hit |
|
|
|
Arguments: |
|
hand: list of strings representing the cards in a hand |
|
Return: |
|
list of strings optimized for strategy_chart |
|
""" |
|
total_aces = len([card for card in hand if card == 'A']) # total number of aces |
|
if total_aces == 0 or len(hand) == 2 and total_aces == 2: # return if there are no aces or if hand is a pair of aces |
|
return hand |
|
hand = sorted(hand, reverse=True) # sort hand in reverse alphabetical order so aces are first |
|
alt_total = sum([int(card) for card in hand[1:] if card != 'A']) + total_aces - 1 # calculate alternate sum assuming other aces are 1 |
|
if alt_total <= 9: # if alt_total is 9 or less then it will be in strategy_chart |
|
return [str(alt_total), 'A'] |
|
return hand # return hand otherwise |
|
|
|
|
|
def hand_sum(hand): |
|
""" |
|
Sums all of the cards in the hand |
|
|
|
Arguments: |
|
hand: list of strings representing the cards in a hand |
|
Returns: |
|
integer with the sum of the hands |
|
""" |
|
non_ace_total = sum([int(card) for card in hand if card != 'A']) # total of all non ace cards |
|
total_aces = len([card for card in hand if card == 'A']) # number of aces in hand |
|
sum_options = [non_ace_total + (i * 11) + (total_aces - i) for i in range(total_aces + 1)] # list of possible sums |
|
return sorted([option for option in sum_options if option <= 21], reverse=True)[0] # returns the greatest sum 21 or less |
|
|
|
|
|
def digest_strategy(recommendation, surrender=False, double=False): |
|
""" |
|
Figures out what recommendation to show based on the rules of the game |
|
|
|
Arguments: |
|
recommendation: a string with the recommendation |
|
surrender: a boolean indicating whether surrender is a legal move |
|
double: a boolean indicating whether doubling is a legal move |
|
Return: |
|
str containing the recommended move |
|
""" |
|
if recommendation[0] == 'X' and surrender or recommendation[0] == 'D' and double: # checks if the first move is not legal |
|
return legend[recommendation[0]] # sends initial move if option is legal |
|
else: |
|
return legend[recommendation[1]] # sends second move if option is illegal |
|
|
|
|
|
def action(dealer, player_hand): |
|
""" |
|
Determines the action the player should take based on the cards dealt to them |
|
|
|
Arguments: |
|
dealer: a string representing the dealers card |
|
player_hand: a list containing the players two cards |
|
Return: |
|
str containing the recommended move |
|
""" |
|
total_cards = len(player_hand) # Stores the original number of cards in players hand |
|
player_hand = sorted(optimize_hand(player_hand)) |
|
player_sum = hand_sum(player_hand) |
|
if ','.join(player_hand) in strategy_chart: # checks if the cards are in the strategy_chart |
|
return strategy_chart[','.join(player_hand)][dealer] |
|
elif str(player_sum) in strategy_chart: |
|
if total_cards > 2: |
|
# if the player has more than two cards then you only return the always legal move |
|
# if this code is reached it will be impossible to get a Split response so we do not have to worry about that scenario |
|
return strategy_chart[str(player_sum)][dealer][-1] |
|
else: |
|
return strategy_chart[str(player_sum)][dealer] |
|
elif player_sum <= 8: |
|
return 'H' # hand is not in strategy_chart and player_sum 8 or less |
|
else: |
|
return 'S' # hand is not in the strategy_chart player_sum 18 or more |
|
|
|
if __name__ == '__main__': |
|
parser = argparse.ArgumentParser(usage='%(prog)s [-h] [-D] [-X] dealer player player [player ...]', |
|
description='Enter any card values for the dealer\'s card and player\'s hand') |
|
parser.add_argument('-D', '--double', |
|
action='store_true', |
|
help='allow doubling') |
|
parser.add_argument('-X', '--surrender', |
|
action='store_true', |
|
help='allow surrendering') |
|
parser.add_argument('dealer', |
|
type=card, |
|
help='A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K') |
|
parser.add_argument('first', |
|
metavar='player', |
|
type=card, |
|
help='A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K') |
|
parser.add_argument('others', |
|
metavar='player', |
|
nargs='+', |
|
type=card, |
|
help=argparse.SUPPRESS) |
|
args = parser.parse_args() |
|
player = list(args.others) # player is mutable list of players hand |
|
player.append(args.first) |
|
print(digest_strategy(action(args.dealer, player), surrender=args.surrender, double=args.double)) |