Files
basic-computer-games/10_Blackjack/python/blackjack.py
Martin Thoma c500424956 Simplify Python Code
print_with_tab / print_with_whitespace is trivial with Python
string formatting and was mostly used in only 2 lines.
2022-04-02 07:32:09 +02:00

492 lines
15 KiB
Python

"""
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) -> None:
"""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") -> None:
"""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) -> "Decks":
"""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) -> None:
"""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) -> None:
"""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":
print_instructions()
print()
return Game(players=players, decks=Decks.new(), games_played=0)
def _print_stats(self) -> None:
"""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) -> None:
"""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()
print_welcome_screen()
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 and player.bet == 0:
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() -> None:
game: Game
print_welcome_screen()
# 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
char = "y"
while char == "y":
game.play_game()
char = get_char_from_user_input("Play Again?", ["y", "n"])
def print_welcome_screen() -> None:
print(
"""
BLACK JACK
CREATIVE COMPUTING MORRISTOWN, NEW JERSEY
\n\n"""
)
def print_instructions() -> None:
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() -> None:
"""clear std out"""
print("\x1b[2J\x1b[0;0H")
if __name__ == "__main__":
main()