diff --git a/10_Blackjack/python/blackjack.py b/10_Blackjack/python/blackjack.py new file mode 100644 index 00000000..d1804be8 --- /dev/null +++ b/10_Blackjack/python/blackjack.py @@ -0,0 +1,505 @@ +""" +Blackjack + +Ported by Martin Thoma in 2022, +using the rust implementation of AnthonyMichaelTDM +""" + +import enum +import random +from dataclasses import dataclass +from typing import List, NamedTuple + + +class PlayerType(enum.Enum): + Player = "Player" + Dealer = "Dealer" + + +class Play(enum.Enum): + Stand = enum.auto() + Hit = enum.auto() + DoubleDown = enum.auto() + Split = enum.auto() + + +class Card(NamedTuple): + name: str + + @property + def value(self) -> int: + """ + returns the value associated with a card with the passed name + return 0 if the passed card name doesn't exist + """ + return { + "ACE": 11, + "2": 2, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7, + "8": 8, + "9": 9, + "10": 10, + "JACK": 10, + "QUEEN": 10, + "KING": 10, + }.get(self.name, 0) + + +class Hand(NamedTuple): + cards: List[Card] + + def add_card(self, card: Card): + """add a passed card to this hand""" + self.cards.append(card) + + def get_total(self) -> int: + """returns the total points of the cards in this hand""" + total: int = 0 + for card in self.cards: + total += int(card.value) + + # if there is an ACE, and the hand would otherwise bust, + # treat the ace like it's worth 1 + if total > 21 and any(card.name == "ACE" for card in self.cards): + total -= 10 + + return total + + def discard_hand(self, deck: "Decks"): + """adds the cards in hand into the discard pile""" + _len = len(self.cards) + for _i in range(_len): + if len(self.cards) == 0: + raise ValueError("hand empty") + deck.discard_pile.append(self.cards.pop()) + + +class Decks(NamedTuple): + deck: List[Card] + discard_pile: List[Card] + + @classmethod + def new(cls): + """creates a new full and shuffled deck, and an empty discard pile""" + # returns a number of full decks of 52 cards, shuffles them + deck = Decks(deck=[], discard_pile=[]) + number_of_decks = 3 + + # fill deck + for _n in range(number_of_decks): + # fill deck with number_of_decks decks worth of cards + for card_name in CARD_NAMES: + # add 4 of each card, totaling one deck with 4 of each card + for _ in range(4): + deck.deck.append(Card(name=card_name)) + + deck.shuffle() + return deck + + def shuffle(self): + """shuffles the deck""" + random.shuffle(self.deck) + + def draw_card(self) -> Card: + """ + draw card from deck, and return it + if deck is empty, shuffles discard pile into it and tries again + """ + if len(self.deck) == 0: + _len = len(self.discard_pile) + + if _len > 0: + # deck is empty, shuffle discard pile into deck and try again + print("deck is empty, shuffling") + for _i in range(_len): + if len(self.discard_pile) == 0: + raise ValueError("discard pile is empty") + self.deck.append(self.discard_pile.pop()) + self.shuffle() + return self.draw_card() + else: + # discard pile and deck are empty, should never happen + raise Exception("discard pile empty") + else: + card = self.deck.pop() + return card + + +@dataclass +class Player: + hand: Hand + balance: int + bet: int + wins: int + player_type: PlayerType + index: int + + @classmethod + def new(cls, player_type: PlayerType, index: int) -> "Player": + """creates a new player of the given type""" + return Player( + hand=Hand(cards=[]), + balance=STARTING_BALANCE, + bet=0, + wins=0, + player_type=player_type, + index=index, + ) + + def get_name(self) -> str: + return f"{self.player_type}{self.index}" + + def get_bet(self): + """gets a bet from the player""" + if PlayerType.Player == self.player_type: + if self.balance < 1: + print(f"{self.get_name()} is out of money :(") + self.bet = 0 + self.bet = get_number_from_user_input( + f"{self.get_name()}\tWhat is your bet", 1, self.balance + ) + + def hand_as_string(self, hide_dealer: bool) -> str: + """ + returns a string of the players hand + + if player is a dealer, returns the first card in the hand followed + by *'s for every other card + if player is a player, returns every card and the total + """ + if not hide_dealer: + s = "" + for cards_in_hand in self.hand.cards[::-1]: + s += f"{cards_in_hand.name}\t" + s += f"total points = {self.hand.get_total()}" + return s + else: + if self.player_type == PlayerType.Dealer: + s = "" + for c in self.hand.cards[1::-1]: + s += f"{c.name}\t" + return s + elif self.player_type == PlayerType.Player: + s = "" + for cards_in_hand in self.hand.cards[::-1]: + s += f"{cards_in_hand.name}\t" + s += f"total points = {self.hand.get_total()}" + return s + raise Exception("This is unreachable") + + def get_play(self) -> Play: + """get the players 'play'""" + # do different things depending on what type of player this is: + # if it's a dealer, use an algorithm to determine the play + # if it's a player, ask user for input + if self.player_type == PlayerType.Dealer: + if self.hand.get_total() > 16: + return Play.Stand + else: + return Play.Hit + elif self.player_type == PlayerType.Player: + valid_results: List[str] + if len(self.hand.cards) > 2: + # if there are more than 2 cards in the hand, + # at least one turn has happened, so splitting and + # doubling down are not allowed + valid_results = ["s", "h"] + else: + valid_results = ["s", "h", "d", "/"] + play = get_char_from_user_input("\tWhat is your play?", valid_results) + if play == "s": + return Play.Stand + elif play == "h": + return Play.Hit + elif play == "d": + return Play.DoubleDown + elif play == "/": + return Play.Split + else: + raise ValueError(f"got invalid character {play}") + raise Exception("This is unreachable") + + +@dataclass +class Game: + players: List[Player] # last item in this is the dealer + decks: Decks + games_played: int + + @classmethod + def new(cls, num_players: int) -> "Game": + players: List[Player] = [] + + # add dealer + players.append(Player.new(PlayerType.Dealer, 0)) + # create human player(s) (at least one) + players.append(Player.new(PlayerType.Player, 1)) + for i in range(2, num_players): # one less than num_players players + players.append(Player.new(PlayerType.Player, i)) + + if get_char_from_user_input("Do you want instructions", ["y", "n"]) == "y": + instructions() + print() + + return Game(players=players, decks=Decks.new(), games_played=0) + + def _print_stats(self): + """prints the score of every player""" + print(f"{self.stats_as_string()}") + + def stats_as_string(self) -> str: + """returns a string of the wins, balance, and bets of every player""" + s = "" + for p in self.players: + # format the presentation of player stats + if p.player_type == PlayerType.Dealer: + s += f"{p.get_name()} Wins:\t{p.wins}\n" + elif p.player_type == PlayerType.Player: + s += f"{p.get_name()} " + s += f"Wins:\t{p.wins}\t\t" + s += f"Balance:\t{p.balance}\t\tBet\t{p.bet}\n" + return f"Scores:\n{s}" + + def play_game(self): + """plays a round of blackjack""" + game = self.games_played + player_hands_message: str = "" + + # deal two cards to each player + for _i in range(2): + for player in self.players: + player.hand.add_card(self.decks.draw_card()) + + # get everyones bets + for player in self.players: + player.get_bet() + scores = self.stats_as_string() + + # play game for each player + for player in self.players: + # turn loop, ends when player finishes their turn + while True: + clear() + welcome() + print(f"\n\t\t\tGame {game}") + print(scores) + print(player_hands_message) + print(f"{player.get_name()} Hand:\t{player.hand_as_string(True)}") + + if PlayerType.Player == player.player_type: + # player isn't the dealer + if player.bet == 0: # player is out of money + break + + # play through turn + # check their hand value for a blackjack(21) or bust + score = player.hand.get_total() + if score >= 21: + if score == 21: + print("\tBlackjack! (21 points)") + else: + print(f"\tBust ({score} points)") + break + + # get player move + play = player.get_play() + # process play + if play == Play.Stand: + print(f"\t{play}") + break + elif play == Play.Hit: + print(f"\t{play}") + player.hand.add_card(self.decks.draw_card()) + elif play == Play.DoubleDown: + print(f"\t{play}") + + # double their balance if there's enough money, + # othewise go all-in + if player.bet * 2 < player.balance: + player.bet *= 2 + else: + player.bet = player.balance + player.hand.add_card(self.decks.draw_card()) + elif play == Play.Split: + pass + + # add player to score cache thing + player_hands_message += ( + f"{player.get_name()} Hand:\t{player.hand_as_string(True)}\n" + ) + + # determine winner + top_score = 0 + + # player with the highest points + num_winners = 1 + + non_burst_players = [ + player for player in self.players if player.hand.get_total() <= 21 + ] + for player in non_burst_players: + score = player.hand.get_total() + if score > top_score: + top_score = score + num_winners = 1 + elif score == top_score: + num_winners += 1 + + # show winner(s) + top_score_players = [ + player + for player in non_burst_players + if player.hand.get_total() == top_score + ] + for x in top_score_players: + print(f"{x.get_name()} ") + x.wins += 1 + # increment their wins + if num_winners > 1: + print(f"all tie with {top_score}\n\n\n") + else: + print( + f"wins with {top_score}!\n\n\n", + ) + + # handle bets + # remove money from losers + losers = [ + player for player in self.players if player.hand.get_total() != top_score + ] + for loser in losers: + loser.balance -= loser.bet + # add money to winner + winners = [ + player for player in self.players if player.hand.get_total() == top_score + ] + for winner in winners: + winner.balance += winner.bet + + # discard hands + for player in self.players: + player.hand.discard_hand(self.decks) + + # increment games_played + self.games_played += 1 + + +CARD_NAMES: List[str] = [ + "ACE", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "JACK", + "QUEEN", + "KING", +] +STARTING_BALANCE: int = 100 + + +def main(): + game: Game + + # print welcome message + welcome() + + # create game + game = Game.new( + get_number_from_user_input("How many players should there be", 1, 7) + ) + + # game loop, play game until user wants to stop + while True: + # play round + game.play_game() + + # ask if they want to play again + char = None + while char not in ["y", "n"]: + char = input("Play Again? (y/n)").lower() + if char == "y": + continue + else: + break + + +def welcome(): + """prints the welcome screen""" + # welcome message + print( + """ + BLACK JACK + CREATIVE COMPUTING MORRISTOWN, NEW JERSEY + \n\n""" + ) + + +def instructions(): + """prints the instructions""" + print( + """ + THIS IS THE GAME OF 21. AS MANY AS 7 PLAYERS MAY PLAY THE + GAME. ON EACH DEAL, BETS WILL BE ASKED FOR, AND THE + PLAYERS' BETS SHOULD BE TYPED IN. THE CARDS WILL THEN BE + DEALT, AND EACH PLAYER IN TURN PLAYS HIS HAND. THE + FIRST RESPONSE SHOULD BE EITHER 'D', INDICATING THAT THE + PLAYER IS DOUBLING DOWN, 'S', INDICATING THAT HE IS + STANDING, 'H', INDICATING HE WANTS ANOTHER CARD, OR '/', + INDICATING THAT HE WANTS TO SPLIT HIS CARDS. AFTER THE + INITIAL RESPONSE, ALL FURTHER RESPONSES SHOULD BE 'S' OR + 'H', UNLESS THE CARDS WERE SPLIT, IN WHICH CASE DOUBLING + DOWN IS AGAIN PERMITTED. IN ORDER TO COLLECT FOR + BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'. + NUMBER OF PLAYERS + + NOTE:'/' (splitting) is not currently implemented, and does nothing + + PRESS ENTER TO CONTINUE + """ + ) + input() + + +def get_number_from_user_input(prompt: str, min_value: int, max_value: int) -> int: + """gets a int integer from user input""" + # input loop + user_input = None + while user_input is None or user_input < min_value or user_input > max_value: + raw_input = input(prompt + f" ({min_value}-{max_value})? ") + + try: + user_input = int(raw_input) + if user_input < min_value or user_input > max_value: + print("Invalid input, please try again") + except ValueError: + print("Invalid input, please try again") + return user_input + + +def get_char_from_user_input(prompt: str, valid_results: List[str]) -> str: + """returns the first character they type""" + user_input = None + while user_input not in valid_results: + user_input = input(prompt + f" {valid_results}? ").lower() + if user_input not in valid_results: + print("Invalid input, please try again") + assert user_input is not None + return user_input + + +def clear(): + """clear std out""" + print("\x1b[2J\x1b[0;0H") + + +if __name__ == "__main__": + main()