mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-22 07:10:42 -08:00
Python version of Blackjack
This commit is contained in:
505
10_Blackjack/python/blackjack.py
Normal file
505
10_Blackjack/python/blackjack.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user