diff --git a/README.md b/README.md index e5d7b00..85651f1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,150 @@ -# Blackjack +Alan Grissett +alan.grissett@gmail.com -* [Part 1](README_1.md) -* [Part 2](README_2.md) \ No newline at end of file +The Iron Yard - Durham +Python Development - Cohort 3 +[ Key: + + - Implemented + * - Not Yet Implemented + ? - Unsure if should be included in class + \ - Planned But Low Priority +] + +Run Instructions: Run from the main blackjack directory using the command "python -m blackjack" + +Blackjack Project + +The goal of this project is to complete a fully functional console based +version of Blackjack. The game will allow the player to select options for +the rules used in the play of the game - including the number of decks used, +dealer hit/stand on soft 17, early/late surrender, hit/resplit aces, +insurance, double on 9/10/11 or 10/11 only and no double on split. + +The game will be divided into classes for each component of the game. +The initial class plan is as follows and is subject to change: + + ++ : The Card class will contain the information about a single card in the +deck. Cards will have attributes for rank, suit and value. Numbered cards +are worth their face value, while Jacks, Queens and Kings are worth 10. Aces +are worth 1 or 11 depending on the player's current hand value. The Card +class will provide a method for updating the value of the ace if requested +to do so by the game. + +The Card objects will be created and stored by the Deck class and will +subsequently be distributed to Hand objects assigned to the player and +dealer respectively. + +To Do: ++ - Assign class attributes ++ - Add method to evaluate the value of Card ++ - Add Unicode representations of suits ++ - Add method for swapping value of Aces + + ++ : The Deck class will be an object containing a collection of at least +one full deck of cards upon initialization. The Deck will construct a list +containing at least one of each card in a standard playing deck and then +use a random number generator to randomize the location of the cards within +the list. The Deck defaults to having a single set of 52 cards, but has an +optional argument to contain any number of decks (with a maximum of 8). +The Deck contains methods to reshuffle the deck and to remove cards from +the list and return their values to the requester. + +To Do: ++ - Generate a list containing Card objects representing a full deck of cards ++ - Randomize card locations within the deck list ++ - Add optional class attribute to increase the number of decks of Card + objects created ++ - Add a method to remove a card from the card list and return the value ++ - Add a method to reshuffle the deck of cards + + ++/ : The player class object will contain the amount of money the +player has remaining to wager and a list of hands (multiple hands on split) +controlled by the player. + +To Do: ++ - Add class attributes ++ - Add methods to increment or decrement remaining money ++ - Add name attribute ++ - Add method to save player state so the game can be resumed with current + money amount maintained + + +* : The Dealer class will contain a single Hand object corresponding +to the cards it is dealt. It will also contain a rule set for deciding +whether to hit or stand for it's given hand value. + +To Do: ++ - Add class attribute ++ - Add basic decision rules ++ - Add optional rules based on user selected options + + ++ : The Hand class will contain a list of cards currently associated +with a Player or Dealer. It will have a method for determining and returning +the current value of itself and a method to receive new cards from the deck +and add them to the class's Card list. + +To Do: ++ - Add class attribute and initialization ++ - Add method to evaluate value of cards ++ - Add method to request change in the value of an ace if present in the hand ++ - Add method to receive Card objects and add them to the Hand's Card list + + +* : The GameOptions class will store all of the user selected +rules for gameplay. Attributes will include an integer value between 1 and +8 representing the number of decks to be used in the game, as well as a +series of Boolean values with default values that will activate or deactivate +certain rule variations. + +To Do: ++ - Add basic list of class attributes, starting with number of decks ++ - Add in common rule variations. No hit on soft 17 and Late/Early surrender ++ - Add in advanced rule variations + + ++ : The Game class will be the backbone of the game. It will store +the wager amount made by the player, created and distribute Hand objects to +the Player and Dealer and will control game flow based on hand +values held by the player and dealer. It will have methods to calculate +payouts based off of results of the game. Payouts will be 2:1 for a standard +win and 3:2 on blackjack. The Game class will pass the payout information +for use by the Player class to update the Player's money total. ?May create +the Dealer object and retrieve the ruleset from GameOptions.? + +To Do: ++ - Add list of Game attributes ++ - Add method to distribute Hands to Player and Dealer ++ - Add method to check the status of the game by resolving Hand values ++ - Add method to distribute payouts to the player ++ - Add method for determining available actions for the player ++ - Add method to retrieve the ruleset from GameOptions and create the + Dealer based on currently selected rules. (Maybe in Blackjack class?) + + ++
: The Run class will be the driver script for the game. +It will control all of the program flow and interact with the Game and +Interface Classes to start new hands, request menu displays, request output +to the console from the Interface Class. This file will contain the +main method for the game, and will be the script executed to start the +game. + +To Do: ++ - Add main method to initialize the game and start program flow + + ++ : The Interface class will contain all of the ASCII text that +will be used for in-game card representations, menus and methods for +receiving input from the user. + +To Do: ++ - Design main menu ++ - Design options menu ++ - Add method for updating options based on user input from options menu ++ - Add method for displaying current hands for the dealer and player to + the console ++ - Add method to display win/lose text after each hand ++ - Add control game flow to the interface diff --git a/README_1.md b/README_1.md deleted file mode 100644 index 0795a37..0000000 --- a/README_1.md +++ /dev/null @@ -1,76 +0,0 @@ -# Blackjack, Part I - -## Description - -Plan how to create a game of blackjack. - -## Objectives - -### Learning Objectives - -After completing this assignment, you should understand: - -* Object-oriented design. - -### Performance Objectives - -After completing this assignment, you should be able to: - -* Design a system using responsibilities and collaborators. -* Turn your design into objects. - -## Details - -### Deliverables - -* A Git repo called blackjack containing at least: - * a `README.md` file explaining your design. - * a `lib/blackjack` directory with a file for each of your classes, with - their responsibilities and collaborators described in a comment - * a completed Card and Deck class - * tests for Cards and Decks - -### Requirements - -* Passing unit tests -* No PEP8 or Pyflakes warnings or errors -* Use the project layout from _The Hacker's Guide to Python_ - -## Normal Mode - -Read through [the rules of blackjack](https://en.wikipedia.org/wiki/Blackjack) -carefully. After reading through them, write out the steps to run the game in -outline format. (See the additional resources for more on the rules of -blackjack.) - -After that, go through your steps and find all the actors -- that is, nouns -that take actions. Create class-responsibility-collaborator (CRC) cards for -each and then create empty classes for each of them with the responsibilities -and collaborators at the top as a comment. Here is an example that you might -find in `lib/blackjack/card.py`: - -```py -class Card: - """A playing card. - - Responsibilities: - - * Has a rank and a suit. - * Has a point value. Aces point values depend on the Hand. - - Collaborators: - - * Collected into a Deck. - * Collected into a Hand for each player and a Hand for the dealer. - """ -``` - -Lastly, implement the `Card` and `Deck` classes. - -## Additional Resources - -* Other Blackjack rule summaries: - * http://www.pagat.com/banking/blackjack.html - * http://wizardofodds.com/games/blackjack/basics/#toc-Rules -* [Portland Pattern Repository page on CRC cards](http://c2.com/cgi/wiki?CrcCard) -* [A Brief Tour of Responsibility-Driven Design](http://www.wirfs-brock.com/PDFs/A_Brief-Tour-of-RDD.pdf) diff --git a/README_2.md b/README_2.md deleted file mode 100644 index 1e6c0bb..0000000 --- a/README_2.md +++ /dev/null @@ -1,77 +0,0 @@ -# Blackjack - -## Description - -Implement the game of Blackjack. - -## Objectives - -### Learning Objectives - -After completing this assignment, you should understand: - -* how to track state by using objects - -### Performance Objectives - -After completing this assignment, you should be able to: - -* Build an interactive game -* Test an object-oriented program - -## Details - -### Deliverables - -* A Git repo called blackjack containing at least: - * `README.rst` file explaining how to run your project - * a `requirements.txt` file - * a full suite of tests for your project - -### Requirements - -* Passing unit tests -* No PEP8 or Pyflakes warnings or errors - -## Normal Mode - -Take your notes and code from [the previous project](README_1.md). Using those, -create a game of Blackjack that one person plays on the command line against a -computer dealer, with the following rules: - -* The game should start the player with $100 and bets are $10. -* The only valid moves are hit and stand. -* Allow the player to keep playing as long as they have money. -* The dealer uses one deck in their shoe and reshuffles after each round. - -## Hard Mode - -In addition to the requirements from **Normal Mode**: - -* The dealer uses a shoe of six decks. With a shoe, the dealer uses something called a _cut card_. A plastic card is inserted somewhere near the bottom of the shoe. Once it is hit, the shoe is reshuffled at the end of the round. You can simulate this by reshuffling after there are 26 or less cards left in the shoe. -* The player can choose how much they want to bet before each round. -* Add doubling-down. -* Add surrender (early surrender as opposed to late surrender.) -* Add [insurance](https://en.wikipedia.org/wiki/Blackjack#Insurance). -* Add splitting hands. - -## Nightmare Mode - -In addition to the requirements from **Hard Mode**: - -* Add the ability to choose rulesets, like: - * No surrender/early surrender/late surrender. - * Dealer hits soft 17 vs dealer stands of soft 17. - * Number of decks used in the shoe. - -Each choice should be able to be made separately. - -## Notes - -This is again an assignment with a text-based interface, which can be very hard -to test. You will do best at this assignment by building all the logic for each -piece and testing it well before then adding the interface on top of it. - -## Additional Resources - -* [Building Skills in Object-Oriented Design](http://www.itmaybeahack.com/book/oodesign-python-2.1/html/index.html) diff --git a/blackjack/__init__.py b/blackjack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blackjack/__main__.py b/blackjack/__main__.py new file mode 100644 index 0000000..95e04c0 --- /dev/null +++ b/blackjack/__main__.py @@ -0,0 +1,35 @@ +from blackjack.game import Game +from blackjack.game_options import GameOptions +from blackjack.interface import Interface + + +class Main: + + def main(self): + """This is the launching method for the game. It displays the main menu, + receives menu selection and launches the options menu and the game when + prompted.""" + interface = Interface() + options = GameOptions() + + while True: + selection = 0 + selection = interface.main_menu() + if selection == "1": + name = interface.get_name() + game = Game(options, name) + interface.play_game(game) + elif selection == "2": + game = Game(options, "None") + game = interface.load_game_menu(game) + if game: + interface.play_game(game) + elif selection == "3": + options = interface.options_menu(options) + elif selection == "4": + break + + +if __name__ == '__main__': + + Main().main() diff --git a/blackjack/card.py b/blackjack/card.py new file mode 100644 index 0000000..9f33bd9 --- /dev/null +++ b/blackjack/card.py @@ -0,0 +1,56 @@ +suits = ["hearts", "spades", "clubs", "diamonds"] +ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", + "10", "J", "Q", "K"] + + +class Card: + """Playing card class. Each instance represents a single playing card. + + Responsibilities: + + * Each card has a string rank attribute corresponding to it's value + * Each card has a suit attribute + * Each card has a value determined by the private evaluate_card_value + method + * Ace value defaults to 11 but can be changed to 1 by calling the + swap_ace method. + + Collaborators: + + * Cards are collected into decks of varying sizes + * Cards are distributed to player and dealer hands + """ + suits = {"spades": "♤", + "hearts": "\033[.31m♡\033[.37m", + "clubs": "♧", + "diamonds": "\033[.31m♢\033[.37m"} + + def __init__(self, rank, suit): + self.rank = rank + self.suit = suit + self._evaluate_card_value() + + def __str__(self): + return self.rank + self.suits[self.suit] + + def __repr__(self): + return self.__str__() + + def __eq__(self, other): + return self.value == other.value + + def _evaluate_card_value(self): + """Evaluates the value of the card and sets it as a class attribute""" + if self.rank.isdigit(): + self.value = int(self.rank) + elif self.rank == "A": + self.value = 11 + else: + self.value = 10 + + def swap_ace(self): + """Swaps the value of the ace if requested by the game.""" + if self.value == 11: + self.value = 1 + else: + self.value = 11 diff --git a/blackjack/dealer.py b/blackjack/dealer.py new file mode 100644 index 0000000..0105e9f --- /dev/null +++ b/blackjack/dealer.py @@ -0,0 +1,42 @@ +class Dealer: + """This is the Dealer class, it contains information about the Dealer's + hand and hand value. + + Responsibilities: + + * Stores Hand objects containing Card objects that the dealer controls + * Determines the dealer's action based off of it's hand value + + Interactions: + * Receives hand value information from the Hand class + * Provides feedback about Dealer actions to the Game class + """ + + def __init__(self, hit_soft_17=False): + self.hand = None + self.hit_soft_17 = hit_soft_17 + + def hit(self): + """Determines if the dealer should hit or not. Has option to allow + for hit/no hit on soft 17""" + if not self.hit_soft_17: + return self.hand.get_value() < 17 + else: + if self.hand.get_value() < 17: + return True + elif self.hand.get_value() == 17 and "A" in self.hand.get_ranks(): + return True + else: + return False + + def get_dealer_hand(self): + """Returns the dealer's current hand object""" + return self.hand + + def get_show_card(self): + """Returns the dealer's show card for display in the game""" + return self.hand.cards[0] + + def takes_hit(self, card): + """Appends a dealt card to the dealer's hand""" + self.hand.cards.append(card) diff --git a/blackjack/deck.py b/blackjack/deck.py new file mode 100644 index 0000000..6a132e0 --- /dev/null +++ b/blackjack/deck.py @@ -0,0 +1,40 @@ +from random import shuffle +from blackjack.card import Card, ranks, suits + + +class Deck: + """Deck of cards class. A deck of cards is a collection of card objects. + + Responsibilities: + + * The Deck class generates a series of Card objects corresponding with + the required cards in a complete deck of cards. + * It is also responsible for randomizing the location of the cards + within the deck list. + * Takes an optional argument decks at creation to allow for creation of + a shoe containing any number of decks. + + Collaborators: + + * Deck collects Card Objects + * Deck pops and returns cards from it's card list when requested by + a calling function. + * Deck can be reinitialized by an outside function by calling the + reshuffle method. + """ + + def __init__(self, decks=1): + self.decks = decks + + self.cards = [Card(rank, suit) for suit in suits + for rank in ranks + for deck in range(self.decks)] + shuffle(self.cards) + + def __len__(self): + return len(self.cards) + + def deal(self): + """Pops a single card from the end of the card list and returns + it to the calling function""" + return self.cards.pop() diff --git a/blackjack/game.py b/blackjack/game.py new file mode 100644 index 0000000..b34220d --- /dev/null +++ b/blackjack/game.py @@ -0,0 +1,146 @@ +from blackjack.deck import Deck +from blackjack.hand import Hand +from blackjack.player import Player +from blackjack.dealer import Dealer + + +class Game: + """ This is the main game class. It provides an interface for interaction + between the deck, player and dealer. It receives rule options from the + game options class to properly initialize the dealer to play with the + selected ruleset and properly initialize the deck to have the selected + number of cards. + + Responsibilites: + * Initializes the deck for play + * Initializes the dealer + * Keeps track of the player's wager + * Resolves whether the dealer of player wins a hand + + Collaborators: + * Interacts with the deck class to generate a deck of the correct size + * Interacts with the GameOptions class to determine the ruleset for the + game and initialize the dealer appropriately + * Receives wager from player and transfers winnings to player""" + + def __init__(self, options, name): + self.deck = Deck(options.number_of_decks) + self.player = Player(name) + self.dealer = Dealer(options.hit_soft_17) + self.options = options + + def can_double(self, hand): + """Checks to see if the option to double down is abailable to the + player""" + if self.options.no_double_after_split and len(self.player.hands) > 1: + return False + else: + if self.options.double_9_10_11: + return 9 <= hand.get_value() < 12 and len(hand.cards) == 2 + elif self.options.double_9_10: + return 9 <= hand.get_value() < 11 and len(hand.cards) == 2 + else: + return len(hand.cards) == 2 and not self.player.doubled + + def can_hit(self, hand): + """Checks to see if the player has the option to hit. Only not + available if hitting split aces is disabled and player has split + aces""" + if not self.options.hit_split_aces and (len(self.player.hands) > 1 + and hand.cards[0].rank == "A"): + return False + else: + return True + + def can_split(self, hand): + """Checks to see if the option to split is available to the player""" + if not self.options.resplitting and len(self.player.hands) > 1: + return False + elif not self.options.resplit_aces and (len(self.player.hands) > 1 + and "A" in hand.get_ranks()): + return False + elif self.options.split_by_rank: + return hand.get_ranks()[0] == hand.get_ranks()[1] + else: + return (hand.cards[0] == hand.cards[1] or + hand.get_ranks()[0] == hand.get_ranks()[1]) + + def can_surrender(self, player_hand): + """Checks to see if the options to surrender is available to the + player""" + return (not self.options.no_surrender + and len(player_hand.cards) == 2 + and not len(self.player.hands) > 1) + + def can_insure(self, player_hand, dealer_show_card): + """Checks to see if the options to insure is available to the + player""" + return (len(player_hand.cards) == 2 and dealer_show_card.rank == "A" + and not len(self.player.hands) > 1 and not self.player.insured) + + def create_hands(self, bet): + """Creates the player and dealer hands and assigns them to their + respective objects""" + dealer_cards = [] + player_cards = [] + for _ in range(2): + player_cards.append(self.deck.deal()) + dealer_cards.append(self.deck.deal()) + self.player.hands.append(Hand(bet, player_cards)) + self.player.modify_money(bet * -1) + self.dealer.hand = Hand(0, dealer_cards) + + def check_bust(self, hand): + """Checks to see if a hand has busted""" + return hand.get_value() > 21 + + def check_push(self, player_hand, dealer_hand): + """Compares a player and dealer hand and to see if a set of hands + results in a push state""" + return player_hand.get_value() == dealer_hand.get_value() + + def compare_hands(self, player_hand, dealer_hand): + """Compares the player and dealer hand to resolve the winner""" + return player_hand.get_value() > dealer_hand.get_value() + + def get_available_actions(self, player_hand): + """Generates a dictionary specifying the available actions + for a given hand.""" + actions = {} + actions["hit"] = self.can_hit(player_hand) + actions["split"] = (self.can_split(player_hand) and + self.player.money >= player_hand.bet) + actions["surrender"] = self.can_surrender(player_hand) + actions["double"] = (self.can_double(player_hand) and + self.player.money >= player_hand.bet) + return actions + + def payout(self, player_hand, dealer_hand): + """Distributes money to the player after a hand is resolved and the + player wins.""" + if ((dealer_hand.get_value() == 21 and len(dealer_hand.cards) == 2) + and self.player.insured): + self.player.modify_money(player_hand.bet) + elif player_hand.get_value() == dealer_hand.get_value(): + self.player.modify_money(player_hand.bet) + elif (player_hand.get_value() > dealer_hand.get_value() or + dealer_hand.get_value() > 21): + self.player.modify_money(player_hand.bet * 2) + + def payout_blackjack(self, player_hand): + """Pays out on player blackjack""" + self.player.modify_money(int(player_hand.bet * 2.5)) + + def reshuffle(self): + """Creates a new deck for use by the game""" + self.deck = Deck(self.options.number_of_decks) + + @property + def dealer_hand(self): + """Returns the dealer's hand object.""" + return self.dealer.get_dealer_hand() + + @property + def player_hands(self): + """Returns the list of players Hand objects.""" + return self.player.get_player_hands() diff --git a/blackjack/game_options.py b/blackjack/game_options.py new file mode 100644 index 0000000..907983e --- /dev/null +++ b/blackjack/game_options.py @@ -0,0 +1,26 @@ +class GameOptions: + """ The game options class is responsible for storing all of the rule + variations that the player wishes to use in the game. It contains + boolean variables corresponding to the available rulesets. It also + checks to ensure that contradictory rules are not chosen. + + Responsibilities: + * Maintains a list of rules selected by the player + + Collaborations: + * Rules are updated by the interface + * Rules are passed to the game during initialization + """ + + def __init__(self): + self.number_of_decks = 1 + self.hit_soft_17 = False + self.early_surrender = False + self.resplitting = True + self.resplit_aces = False + self.hit_split_aces = False + self.split_by_rank = False + self.no_surrender = False + self.no_double_after_split = False + self.double_9_10_11 = False + self.double_9_10 = False diff --git a/blackjack/hand.py b/blackjack/hand.py new file mode 100644 index 0000000..71722e5 --- /dev/null +++ b/blackjack/hand.py @@ -0,0 +1,59 @@ +class Hand: + """ Hand class contains the current cards in play for either the player + or dealer. It has methods to return the total value of the cards held + as well as update the value of the Ace based on the current value of + the hand. + + Responsibilities: + * Container for Card objects + * Reporting the combined value of all Card objects held + * Logic to ensure that Aces are counted as the correct value based on + the total value of the current hand. + + Interactions: + * Hand objects are distributed to the player and the dealer. + * Hand objects receive card objects from the deck. + """ + + def __init__(self, bet, cards=[]): + self.cards = cards + self.bet = bet + + def add_cards(self, card): + """Appends a dealt card to the Hand object's cards list""" + self.cards.append(card) + + def get_ranks(self): + """Returns a list of ranks of all cards in the Hand object's + card list""" + card_rank_list = [] + + for card in self.cards: + card_rank_list.append(card.rank) + + return card_rank_list + + def get_card_strings(self): + """Returns a string representation of all cards in the Hand object's + card list""" + card_strings = [] + for card in self.cards: + card_strings.append(str(card)) + cards = "[ " + " ".join(card_strings) + " ]" + return cards + + def get_value(self): + """Returns the total value of all cards in the card list""" + hand_value = 0 + for card in self.cards: + hand_value += card.value + if ((hand_value > 21 and "A" in self.get_ranks()) or + hand_value <= 11 and "A" in self.get_ranks()): + for card in self.cards: + if hand_value > 21 and card.value == 11: + card.swap_ace() + hand_value = self.get_value() + elif hand_value <= 11 and card.value == 1: + card.swap_ace() + hand_value = self.get_value() + return hand_value diff --git a/blackjack/interface.py b/blackjack/interface.py new file mode 100644 index 0000000..17280de --- /dev/null +++ b/blackjack/interface.py @@ -0,0 +1,522 @@ +import pickle +from blackjack.game_options import GameOptions +from random import choice + + +icons = ["\033[.31m♡\033[.37m", "♧", "\033[.31m♢\033[.37m", "♤"] + + +class Interface: + """ Interface class will provide all of the methods of interacting with + the user. It will contain all of the methods for displaying to the + console as well as receiving and checking input from the player. + + Responsibilities: + * Output game information and menus to the player + * Receiving input from the player + * Control game flow + """ + + def __init__(self): + self.game = None + + def check_for_dealer_blackjack(self): + """Checks the dealer's hand for blackjack""" + return self.game.dealer_hand.get_value() == 21 + + def check_for_player_blackjack(self): + """Checks to see if the player has blackjack""" + if (self.game.player_hands[0].get_value() == 21 + and len(self.game.player_hands[0].cards) == 2 + and len(self.game.player_hands) == 1): + print("BLACKJACK!\n") + if self.check_for_dealer_blackjack(): + print("Dealer has BLACKJACK. Push...\n") + self.game.payout(self.game.player_hands[0], + self.game.dealer_hand) + return True + else: + print("Blackjack pays 3:2!\n") + self.game.payout_blackjack(self.game.player_hands[0]) + return True + else: + return False + + def dealer_play(self): + """Dealer's play method. Goes through all dealer turns until the + hit() method of dealer determines it needs to stand or the dealer + busts""" + while self.game.dealer.hit(): + new_card = self.game.deck.deal() + print("Dealer hits, and recieves {}.\n".format(str(new_card))) + self.game.dealer.takes_hit(new_card) + + if self.game.check_bust(self.game.dealer_hand): + print("Dealer busts!\n") + for hand in self.game.player_hands: + if not self.game.check_bust(hand): + self.game.payout(hand, self.game.dealer_hand) + return False + else: + return True + + def evaluate_hands(self): + """Evaluates the final hands of the player and dealer and makes + the appropriate calls the the playout method of the Game class""" + if len(self.game.player_hands) > 1: + self.evaluate_split_hands() + elif self.game.check_push(self.game.player_hands[0], + self.game.dealer_hand): + self.game.payout(self.game.player_hands[0], + self.game.dealer_hand) + print("Push.\n") + self.print_dealer_hand() + else: + self.win_lose() + + def win_lose(self): + """Prints win lose message determined by player and dealer hands""" + if self.game.compare_hands(self.game.player_hands[0], + self.game.dealer_hand): + print("You win!\n") + self.game.payout(self.game.player_hands[0], + self.game.dealer_hand) + self.print_dealer_hand() + else: + print("Dealer wins.\n") + self.print_dealer_hand() + + def split_hand_win_lose(self, hand): + """Win_lose method that iterates through all active player hands and + displays appropriate message for each hand. Takes an enumerated + hand as argument""" + if self.game.compare_hands(hand[1], self.game.dealer_hand): + print("Hand {} wins!".format(hand[0]+1)) + self.game.payout(hand[1], self.game.dealer_hand) + else: + print("Hand {} loses!".format(hand[0]+1)) + + def evaluate_split_hands(self): + """Method for evaluating multiple hands after a split.""" + for hand in enumerate(self.game.player_hands): + if self.game.check_bust(hand[1]): + print("Hand {} busted.".format(hand[0]+1)) + elif self.game.check_push(hand[1], self.game.dealer_hand): + print("Push on hand {}.".format(hand[0]+1)) + self.game.payout(hand[1], self.game.dealer_hand) + else: + self.split_hand_win_lose(hand) + self.print_dealer_hand() + + def execute_selection(self, selection, actions): + """Takes the users selected action and performs it""" + if selection == "H" and actions["hit"]: + new_card = self.game.deck.deal() + print("\nReceived {}\n".format(new_card)) + self.game.player.takes_hit(self.game.player_hands[ + self.current_hand], + new_card) + elif selection == "D" and actions["double"]: + new_card = self.game.deck.deal() + print("\nReceived {}\n".format(new_card)) + self.game.player.doubles(self.game.player_hands[self.current_hand], + new_card) + elif selection == "P" and actions["split"]: + new_cards = [self.game.deck.deal(), self.game.deck.deal()] + self.game.player.splits(self.game.player_hands[self.current_hand], + new_cards) + elif selection == "R" and actions["surrender"]: + self.game.player.surrenders(self.game.player_hands[ + self.current_hand]) + + def initialize_hand(self): + """Initializes the variables needed for the correct flow of the + hand""" + print("\n"*80) + bet = self.get_bet() + print("\n"*80) + + self.game.player.reset_player() + self.game.create_hands(bet) + self.current_hand = 0 + self.dealers_turn = False + self.continue_hand = True + + if len(self.game.deck) < 30: + self.game.reshuffle() + + def offer_actions(self): + """Prints available actions to the screen and allows user to input + their selected action.""" + actions = self.game.get_available_actions( + self.game.player_hands[self.current_hand]) + valid_selection_made = False + while not valid_selection_made: + valid_input = ["S"] + if actions["hit"]: + print("(H)it, ", end="") + valid_input.append("H") + if actions["double"]: + print("(D)ouble Down, ", end="") + valid_input.append("D") + if actions["split"]: + print("S(P)lit, ", end="") + valid_input.append("P") + if actions["surrender"]: + print("Surrende(R), ", end="") + valid_input.append("R") + print("(S)tand") + selection = input("{} ".format(choice(icons))).upper() + valid_selection_made = selection in valid_input + + return selection, actions + + def offer_insurance(self): + """Method that allows the user to buy insurace if the dealer's upcard + is an Ace""" + print("Dealer has an Ace showing. Buy insurance?") + player_choice = "" + while player_choice not in ["Y", "N"]: + player_choice = input("(Y/N) {} ".format(choice(icons))).upper() + if player_choice == "Y": + self.game.player.buys_insurance() + else: + print("Please enter Y or N.") + + def offer_surrender(self): + """Used on early surrender games. Gives the player the option to + surrender before the dealer checks for blackjack.""" + player_choice = "" + while player_choice not in ["Y", "N"]: + player_choice = input("Surrender? (Y/N) {} ".format( + choice(icons))).upper() + if player_choice == "Y": + return True + elif player_choice == "N": + return False + else: + print("Invalid input.") + + def play_game(self, game): + """Starts each hand an keeps the game going. Offers the user the + choice to play another hand or not.""" + self.game = game + + while self.game.player.money >= 5: + self.play_hand() + player_choice = "" + while player_choice not in ["Y", "N"]: + if self.game.player.money < 5: + print("You don't have enough money to play!") + input() + break + player_choice = input("Play another hand? (Y/N) {} ".format( + choice(icons))).upper() + + if player_choice == "N" and self.game.player.money > 5: + save_choice = "" + while save_choice not in ["Y", "N"]: + save_choice = input( + "Would you like to save? (Y/N) {} ".format( + choice(icons))).upper() + if save_choice == "Y": + self.game.player.save_player_state(self.game.options) + break + + def play_hand(self): + """Hand loop. Continues until the end of the hand. Prints updates + about the status of the hand to the screen and makes method calls + to allow user actions.""" + + self.initialize_hand() + print("Dealing Cards {}\n".format(choice(icons))) + + while self.continue_hand: + self.print_hands() + + if self.resolve_blackjacks(): + break + + selection, actions = self.offer_actions() + self.execute_selection(selection, actions) + + dealers_turn = self.players_turn(selection) + + if dealers_turn: + print("Dealer's turn!") + need_to_compare = self.dealer_play() + if need_to_compare: + self.evaluate_hands() + self.continue_hand = False + else: + self.continue_hand = False + + def players_turn(self, selection): + """Allows the player to make game selections and determines if the + dealer will need to take a turn""" + dealers_turn = False + if self.game.check_bust(self.game.player_hands[self.current_hand]): + if len(self.game.player_hands) == 1: + print("You bust!\n") + self.continue_hand = False + return False + elif len(self.game.player_hands) == self.current_hand + 1: + print("You bust!\n") + for hand in self.game.player_hands: + if hand.get_value() <= 21: + dealers_turn = True + return dealers_turn + else: + print("You bust!\n") + self.current_hand += 1 + elif selection == "R": + print("You surrendered...\n") + self.continue_hand = False + return False + elif selection == "S" or selection == "D": + if len(self.game.player_hands) > self.current_hand + 1: + self.current_hand += 1 + else: + return True + + def print_dealer_hand(self): + """Prints the dealer's hand to the screen""" + print("Dealer's final hand: {}\n".format( + self.game.dealer_hand.get_card_strings())) + + def print_hands(self): + """Displays the dealer's and player's hands to the screen""" + print("Dealer's Hand: [{}, [X]]".format( + self.game.dealer.get_show_card())) + + print("Player's Hand{}: ".format("s" if len(self.game.player_hands) > 1 + else ""), end="") + card_string = "" + for hand in enumerate(self.game.player_hands): + card_string += "{}{} ".format("*" if hand[0] == self.current_hand + else "", + hand[1].get_card_strings(), end="") + + print(card_string) + print("\nMoney: {}".format(self.game.player.money)) + + def resolve_blackjacks(self): + """Resolves all cases of either or both the player and dealer having + blackjack and makes appropriate payouts. Also calls the offer + insurance method to allow the player to insure before blackjacks + are resolved.""" + if self.check_for_player_blackjack(): + return True + if self.game.options.early_surrender: + if self.offer_surrender(): + self.game.player.surrenders(self.game.player_hands[0]) + print("You surrendered.\n") + print("Dealer had: {}\n".format( + self.game.dealer_hand.get_card_strings())) + return True + elif (self.game.dealer.get_show_card().rank == "A" + and len(self.game.player_hands[0].cards) == 2 + and len(self.game.player_hands) == 1 + and self.game.player.money >= self.game.player_hands[0].bet): + self.offer_insurance() + + if self.check_for_dealer_blackjack(): + print("Dealer has blackjack!\n") + self.game.payout(self.game.player_hands[0], + self.game.dealer_hand) + return True + + return False + + def main_menu(self): + """ Displays the title, programmer info, and main menu for the game. + Takes user input and returns to the calling Game class""" + title_text = ( + ".------..------..------..------..------..------..------..------." + ".------.\n|B.--. ||L.--. ||\033[.31mA\033[.37m.--. ||\033[.31mC" + "\033[.37m.--. ||\033[.31mK\033[.37m.--. ||J.--. ||\033[.31mA" + "\033[.37m.--. ||\033[.31mC\033[.37m.--. ||\033[.31mK\033[.37m.--." + " |\n| :(): || :/\: || \033[.31m(\/)\033[.37m || :\033[.31m/" + "\\\033[.37m: || :\033[.31m/\\\033[.37m: || :(): || \033[.31m(\/" + ")\033[.37m || :\033[.31m/\\\033[.37m: || :\033[.31m/\\\033[.37m" + ": |\n| ()() || (__) || :\033[.31m\/\033[.37m: || :\033[.31m\/" + "\033[.37m: || :\033[.31m\/\033[.37m: || ()() || :\033[.31m\/" + "\033[.37m: || :\033[.31m\/\033[.37m: || :\033[.31m\/\033[.37m: " + "|\n| '--'B|| '--'L|| '--'\033[.31mA\033[.37m|| '--'\033[.31mC" + "\033[.37m|| '--'\033[.31mK\033[.37m|| '--'J|| '--'\033[.31mA" + "\033[.37m|| '--'\033[.31mC\033[.37m|| '--'\033[.31mK\033[.37m|\n" + "`------'`------'`------'`------'`------'`------'`------'`------'" + "`------'") + + info = ("Programmed by: Alan Grissett\n" + "The Iron Yard - Durham\n" + "January 2015\n") + + menu_text = ("<><><><><><><><><><><><><><><><><><><><><><>" + "<><><><><><><><><><><><><><") + + print("\n"*80, menu_text) + print(title_text) + print("\n", menu_text) + print(info) + print("Main Menu:") + print("\033[.31m♡\033[.37m 1 - New Game") + print("♤ 2 - Continue Game") + print("\033[.31m♢\033[.37m 3 - Set Game Options") + print("♧ 4 - Quit") + print("-----------------------") + selection = input("{} ".format(choice(icons))) + + return selection + + def get_bet(self): + """Prompts the user to select a bet amount in multiples of 5, not + exceeding 20""" + print("\n"*80) + while True: + print("How much would you like to wager on the next hand? ") + print("Please wager in multiples of 5. Maximum bet is 20.") + try: + print("Type \"O\" to go to the options menu.") + print("You currently have {} dollars.".format( + self.game.player.money)) + bet = input("{} ".format(choice(icons))) + if bet.upper() == "O": + self.options_menu(self.game.options) + continue + bet = int(bet) + if (bet % 5 is not 0 + or bet < 0 + or bet > 20 + or bet > self.game.player.money): + raise ValueError + else: + return bet + except ValueError: + print("Sorry, that's not a valid bet.") + + def get_name(self): + """Prompts the user to input a name, used for the creation of a + Player object.""" + print("\n"*80) + print("Great! Let's play some Blackjack!") + name = input("Please enter your name {} ".format(choice(icons))) + return name + + def options_menu(self, options): + """Displays the options menu and updates option variables to reflect + user selections.""" + selection = "" + while selection is not "Q": + selection = "" + print("\n"*80) + print("Game Options:\n") + print("1 - Number of Decks: {}".format(options.number_of_decks)) + print("2 - Dealer Hits on Soft 17: {}".format("Enabled" if + options.hit_soft_17 + else "Disabled")) + print("3 - Early Surrender: {}".format("Enabled" if + options.early_surrender + else "Disabled")) + print("4 - No Surrender: {}".format("Enabled" if + options.no_surrender + else "Disabled")) + print("5 - Resplitting Allowed: {}".format("Enabled" if + options.resplitting + else "Disabled")) + print("6 - Resplit Aces: {}".format("Enabled" if + options.resplit_aces + else "Disabled")) + print("7 - Hit Split Aces: {}".format("Enabled" if + options.hit_split_aces + else "Disabled")) + print("8 - Split By Rank/Value: {}".format("Rank" if + options.split_by_rank + else "Value")) + print("9 - No Double After Split: ", end="") + print("{}".format("Enabled" if options.no_double_after_split + else "Disabled")) + print("10 - Double on 9-10-11 ", end="") + print("Only: {}".format("Enabled" if options.double_9_10_11 + else "Disabled")) + print("11 - Double on 9-10 Only: {}".format("Enabled" if + options.double_9_10 + else "Disabled")) + print("R - Reset to Defaults") + print("Q - Return to Previous Screen") + selection = input("{} ".format(choice(icons))) + + if selection.upper() == "Q": + return options + else: + options = self.options_menu_input(selection, options) + + def options_menu_input(self, selection, options): + """ This method receives a selection from the options menu + and updates the game options object to reflect the desired + ruleset selected by the player.""" + if selection == "1": + try: + decks = int(input("Choose number of decks (8 max){} ".format( + choice(icons)))) + if decks < 1 or decks > 8: + raise ValueError + options.number_of_decks = decks + except ValueError: + input("Invalid input. Press enter to continue.") + elif selection == "2": + options.hit_soft_17 = not options.hit_soft_17 + elif selection == "3": + options.early_surrender = not options.early_surrender + if options.no_surrender: + options.no_surrender = False + elif selection == "4": + options.no_surrender = not options.no_surrender + if options.early_surrender: + options.early_surrender = False + elif selection == "5": + options.resplitting = not options.resplitting + elif selection == "6": + options.resplit_aces = not options.resplit_aces + elif selection == "7": + options.hit_split_aces = not options.hit_split_aces + elif selection == "8": + options.split_by_rank = not options.split_by_rank + elif selection == "9": + options.no_double_after_split = (not + options.no_double_after_split) + elif selection == "10": + options.double_9_10_11 = not options.double_9_10_11 + if options.double_9_10: + options.double_9_10 = False + elif selection == "11": + options.double_9_10 = not options.double_9_10 + if options.double_9_10_11: + options.double_9_10_11 = False + elif selection.upper() == "R": + options = GameOptions() + + return options + + def load_game_menu(self, game): + try: + with open("saves.dat", "rb") as save_file: + data = pickle.load(save_file) + for player in enumerate(data): + print("{} - {} ${}".format(player[0]+1, + player[1]["name"], + player[1]["money"])) + except FileNotFoundError: + print("No save games found!") + input() + return + while True: + try: + selection = int(input("Select player to load {} ".format( + choice(icons)))) - 1 + game.player.load_player_state(data[selection]["hash"], game) + break + except (ValueError, IndexError): + print("Invalid selection.") + + return game diff --git a/blackjack/player.py b/blackjack/player.py new file mode 100644 index 0000000..9470ad8 --- /dev/null +++ b/blackjack/player.py @@ -0,0 +1,123 @@ +from blackjack.hand import Hand +from datetime import datetime +import pickle +import hashlib + + +class Player: + """This is the player class, it contains a list of hands controlled by the + player and keeps track of how much money the player has won. + + Responsibilities: + + * Stores Hand objects containing Card objects that the player controls + * Keeps track of players betting money + + Collaborations: + + * Informs the game of amount of money player has remaining to wager + """ + + def __init__(self, name): + self.name = name + self.hands = [] + self.money = 100 + self.insured = False + self.doubled = False + self.player_hash = None + + def get_player_hands(self): + return self.hands + + def buys_insurance(self): + """Modifys the player's money if they choose to buy insurance when + offered""" + self.modify_money(int(-.5*self.hands[0].bet)) + self.insured = True + + def modify_money(self, value): + """Updates the player's money based on the supplied value. To + decrease money, value should be negative""" + if self.money + value >= 0: + self.money += value + else: + raise ValueError("Player money cannot be negative.") + + def takes_hit(self, hand, card): + """Adds the supplied card to the specified hand of the player. + Multiple hands may exist if player splits""" + hand.add_cards(card) + + def splits(self, hand, cards): + """Splits the player's hand into two hands, each containing one of + the cards from the original hand, plus a newly dealt card.""" + self.modify_money(-1*hand.bet) + new_hand = Hand(hand.bet, [hand.cards[1], cards[0]]) + hand.cards.pop(1) + hand.add_cards(cards[1]) + self.hands.append(new_hand) + + def doubles(self, hand, card): + """Doubles the player's bet on the current hand.""" + self.modify_money(-1*hand.bet) + hand.bet *= 2 + self.doubled = True + self.takes_hit(hand, card) + + def surrenders(self, hand): + """Returns half of the player's bet if they choose to surrender the + hand.""" + self.modify_money(int(.5*hand.bet)) + + def reset_player(self): + """Resets the players list of controlled hands""" + self.hands = [] + self.insured = False + self.doubled = False + + def save_player_state(self, options): + """Saves the player's progress for later play""" + if not self.player_hash: + self.player_hash = hashlib.md5(bytes(str(datetime.now()), + "UTF-8")).hexdigest() + player_data = {"name": self.name, + "money": self.money, + "hash": self.player_hash, + "options": options + } + try: + with open("saves.dat", "rb") as read_file: + data = pickle.load(read_file) + for saved_player in data: + if self.player_hash == saved_player["hash"]: + saved_player["money"] = self.money + saved_player["options"] = options + break + else: + data.append(player_data) + + with open("saves.dat", "wb") as write_file: + pickle.dump(data, write_file) + + except FileNotFoundError: + with open("saves.dat", "xb") as write_file: + data = [{"name": self.name, + "money": self.money, + "hash": self.player_hash, + "options": options}] + pickle.dump(data, write_file) + + def load_player_state(self, player_hash, game): + """Loads the player's previous information from saved game file.""" + try: + with open("saves.dat", "rb") as read_file: + data = pickle.load(read_file) + for saved_player in data: + if player_hash == saved_player["hash"]: + self.name = saved_player["name"] + self.money = saved_player["money"] + self.player_hash = saved_player["hash"] + game.options = saved_player["options"] + break + except IOError: + print("Error loading game...Sorry.") diff --git a/blackjack/tests/__init__.py b/blackjack/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blackjack/tests/test_card.py b/blackjack/tests/test_card.py new file mode 100644 index 0000000..af13b99 --- /dev/null +++ b/blackjack/tests/test_card.py @@ -0,0 +1,67 @@ +from blackjack.card import Card +"""Test functions for the card class""" + + +def test_card_attributes(): + new_card_A = Card("A", "clubs") + new_card_2 = Card("2", "hearts") + new_card_3 = Card("3", "spades") + new_card_4 = Card("4", "diamonds") + new_card_5 = Card("5", "clubs") + new_card_6 = Card("6", "hearts") + new_card_7 = Card("7", "spades") + new_card_8 = Card("8", "diamonds") + new_card_9 = Card("9", "clubs") + new_card_10 = Card("10", "hearts") + new_card_J = Card("J", "spades") + new_card_Q = Card("Q", "diamonds") + new_card_K = Card("K", "clubs") + + assert new_card_A.rank == "A" + assert new_card_2.rank == "2" + assert new_card_3.rank == "3" + assert new_card_4.rank == "4" + assert new_card_5.rank == "5" + assert new_card_6.rank == "6" + assert new_card_7.rank == "7" + assert new_card_8.rank == "8" + assert new_card_9.rank == "9" + assert new_card_10.rank == "10" + assert new_card_J.rank == "J" + assert new_card_Q.rank == "Q" + assert new_card_K.rank == "K" + + assert new_card_A.suit == "clubs" + assert new_card_2.suit == "hearts" + assert new_card_3.suit == "spades" + assert new_card_4.suit == "diamonds" + assert new_card_5.suit == "clubs" + assert new_card_6.suit == "hearts" + assert new_card_7.suit == "spades" + assert new_card_8.suit == "diamonds" + assert new_card_9.suit == "clubs" + assert new_card_10.suit == "hearts" + assert new_card_J.suit == "spades" + assert new_card_Q.suit == "diamonds" + assert new_card_K.suit == "clubs" + + assert new_card_A.value == 11 + assert new_card_2.value == 2 + assert new_card_3.value == 3 + assert new_card_4.value == 4 + assert new_card_5.value == 5 + assert new_card_6.value == 6 + assert new_card_7.value == 7 + assert new_card_8.value == 8 + assert new_card_9.value == 9 + assert new_card_10.value == 10 + assert new_card_J.value == 10 + assert new_card_Q.value == 10 + assert new_card_K.value == 10 + + +def test_swap_ace(): + new_card = Card("A", "hearts") + assert new_card.value == 11 + new_card.swap_ace() + assert new_card.value == 1 diff --git a/blackjack/tests/test_dealer.py b/blackjack/tests/test_dealer.py new file mode 100644 index 0000000..5f39fd1 --- /dev/null +++ b/blackjack/tests/test_dealer.py @@ -0,0 +1,39 @@ +from blackjack.dealer import Dealer +from blackjack.card import Card +from blackjack.hand import Hand +from blackjack.game_options import GameOptions +from blackjack.game import Game + + +def test_dealer_hit(): + hand = Hand(0, [Card("6", "clubs"), Card("5", "hearts")]) + dealer = Dealer() + + dealer.hand = hand + + assert dealer.hit() + + +def test_dealer_does_not_hit(): + hand = Hand(0, [Card("10", "clubs"), Card("7", "hearts")]) + dealer = Dealer() + + dealer.hand = hand + + assert not dealer.hit() + + +def test_dealer_hits_soft_17(): + hand = Hand(0, [Card("6", "clubs"), Card("A", "hearts")]) + dealer = Dealer(hit_soft_17=True) + + dealer.hand = hand + + assert dealer.hit() + + +def test_dealer_takes_hit(): + options = GameOptions() + new_game = Game(options, "Alan") + new_game.dealer.hand = Hand(10, [Card("2", "spades"), Card("J", "clubs")]) + new_game.dealer.takes_hit(new_game.deck.deal()) diff --git a/blackjack/tests/test_deck.py b/blackjack/tests/test_deck.py new file mode 100644 index 0000000..efa6411 --- /dev/null +++ b/blackjack/tests/test_deck.py @@ -0,0 +1,30 @@ +from blackjack.deck import Deck +from blackjack.card import Card + + +def test_deck_length(): + new_deck = Deck() + assert len(new_deck.cards) == 52 + + +def test_shoe_creation(): + new_deck = Deck(2) + assert len(new_deck.cards) == 104 + new_deck_2 = Deck(4) + assert len(new_deck_2.cards) == 208 + + +def test_shuffle_deck(): + suits = ["hearts", "spades", "clubs", "diamonds"] + ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", + "10", "J", "Q", "K"] + unshuffled_deck = [Card(rank, suit) for suit in suits + for rank in ranks] + new_deck = Deck() + + assert new_deck.cards != unshuffled_deck + + +def test_deal_cards(): + new_deck = Deck() + assert new_deck.deal() diff --git a/blackjack/tests/test_game.py b/blackjack/tests/test_game.py new file mode 100644 index 0000000..1196c5e --- /dev/null +++ b/blackjack/tests/test_game.py @@ -0,0 +1,243 @@ +from blackjack.card import Card +from blackjack.game_options import GameOptions +from blackjack.game import Game +from blackjack.hand import Hand + + +def test_create_hands(): + options = GameOptions() + new_game = Game(options, "Alan") + new_game.create_hands(10) + + assert len(new_game.player.hands[0].cards) == 2 + assert len(new_game.dealer.hand.cards) == 2 + assert len(new_game.deck.cards) == 48 + assert new_game.player.hands[0].bet == 10 + assert new_game.player.money == 90 + + +def test_bust(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("J", "hearts"), Card("Q", "diamonds")]) + assert not new_game.check_bust(hand) + hand.cards.append(Card("K", "spades")) + assert new_game.check_bust(hand) + + +def test_push(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("J", "hearts"), Card("Q", "diamonds")]) + hand2 = Hand(10, [Card("K", "hearts"), Card("J", "diamonds")]) + assert new_game.check_push(hand, hand2) + hand.cards.append(Card("A", "spades")) + assert not new_game.check_push(hand, hand2) + + +def test_can_split_resplitting(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("J", "hearts"), Card("Q", "diamonds")]) + new_game.player.hands.append(hand) + assert new_game.can_split(hand) + new_game.player.hands.append(hand) + assert new_game.can_split(hand) + new_game.options.resplitting = False + assert not new_game.can_split(hand) + new_game.player.hands.pop() + assert new_game.can_split(hand) + + +def test_resplitting_aces(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("A", "hearts"), Card("A", "diamonds")]) + new_game.player.hands.append(hand) + assert new_game.can_split(hand) + new_game.player.hands.append(hand) + print(len(new_game.player.hands)) + assert not new_game.can_split(hand) + new_game.options.resplit_aces = True + assert new_game.can_split(hand) + + +def test_can_split_by_rank(): + options = GameOptions() + options.split_by_rank = True + new_game = Game(options, "Alan") + hand = Hand(10, [Card("Q", "hearts"), Card("Q", "diamonds")]) + assert new_game.can_split(hand) + hand = Hand(10, [Card("Q", "hearts"), Card("10", "diamonds")]) + assert not new_game.can_split(hand) + + +def test_can_split_by_value(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("Q", "hearts"), Card("10", "diamonds")]) + assert new_game.can_split(hand) + hand = Hand(10, [Card("A", "hearts"), Card("A", "diamonds")]) + assert new_game.can_split(hand) + + +def test_can_double_normal(): + options = GameOptions() + options.split_by_rank = True + new_game = Game(options, "Alan") + hand = Hand(10, [Card("6", "clubs"), Card("8", "hearts")]) + assert new_game.can_double(hand) + hand.cards.append(Card("3", "diamonds")) + assert not new_game.can_double(hand) + + +def test_can_double_9_10_11(): + options = GameOptions() + options.double_9_10_11 = True + new_game = Game(options, "Alan") + hand = Hand(10, [Card("6", "clubs"), Card("3", "hearts")]) + assert new_game.can_double(hand) + hand = Hand(10, [Card("6", "clubs"), Card("4", "hearts")]) + assert new_game.can_double(hand) + hand = Hand(10, [Card("6", "clubs"), Card("5", "hearts")]) + assert new_game.can_double(hand) + hand = Hand(10, [Card("6", "clubs"), Card("8", "hearts")]) + assert not new_game.can_double(hand) + + +def test_can_double_9_10(): + options = GameOptions() + options.double_9_10 = True + new_game = Game(options, "Alan") + hand = Hand(10, [Card("6", "clubs"), Card("3", "hearts")]) + assert new_game.can_double(hand) + hand = Hand(10, [Card("6", "clubs"), Card("4", "hearts")]) + assert new_game.can_double(hand) + hand = Hand(10, [Card("6", "clubs"), Card("5", "hearts")]) + assert not new_game.can_double(hand) + hand = Hand(10, [Card("6", "clubs"), Card("8", "hearts")]) + assert not new_game.can_double(hand) + + +def test_can_surrender(): + options = GameOptions() + new_game = Game(options, "Alan") + hand1 = Hand(10, [Card("6", "clubs"), Card("10", "hearts")]) + hand2 = Hand(10, [Card("7", "hearts"), Card("A", "spades")]) + assert new_game.can_surrender(hand1) + new_game.options.no_surrender = True + assert not new_game.can_surrender(hand1) + assert not new_game.can_surrender(hand1) + + +def test_hit_split_aces(): + options = GameOptions() + new_game = Game(options, "Alan") + hand1 = Hand(10, [Card("A", "spades"), Card("6", "hearts")]) + hand2 = Hand(10, [Card("A", "hearts"), Card("7", "spades")]) + new_game.player.hands.append(hand1) + new_game.player.hands.append(hand2) + assert not new_game.can_hit(hand1) + new_game.player.hands.pop() + assert new_game.can_hit(hand1) + + +def test_no_double_after_split(): + options = GameOptions() + new_game = Game(options, "Alan") + hand1 = Hand(10, [Card("A", "spades"), Card("6", "hearts")]) + hand2 = Hand(10, [Card("A", "hearts"), Card("7", "spades")]) + new_game.player.hands.append(hand1) + new_game.player.hands.append(hand2) + assert new_game.can_double(hand1) + new_game.options.no_double_after_split = True + assert not new_game.can_double(hand1) + + +def test_hit(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("A", "spades"), Card("6", "hearts")]) + new_game.player.takes_hit(hand, new_game.deck.deal()) + assert len(hand.cards) == 3 + + +def test_split(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("J", "spades"), Card("J", "clubs")]) + new_game.player.hands.append(hand) + new_game.player.splits(hand, [new_game.deck.deal(), new_game.deck.deal()]) + assert len(new_game.player.hands) == 2 + assert len(hand.cards) == 2 + assert hand.cards[0].rank == "J" + assert new_game.player.hands[1].cards[0].rank == "J" + assert len(new_game.player.hands[1].cards) == 2 + + +def test_double_down(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("2", "spades"), Card("J", "clubs")]) + new_game.player.doubles(hand, new_game.deck.deal()) + assert len(hand.cards) == 3 + assert hand.bet == 20 + assert new_game.player.money == 90 + + +def test_surrender(): + options = GameOptions() + new_game = Game(options, "Alan") + hand = Hand(10, [Card("2", "spades"), Card("J", "clubs")]) + new_game.player.surrenders(hand) + + +def test_payout(): + options = GameOptions() + new_game = Game(options, "Alan") + new_game.create_hands(10) + new_game.player.hands[0] = Hand(10, [Card("4", "spades"), + Card("J", "clubs")]) + new_game.dealer.hand = Hand(10, [Card("3", "spades"), + Card("Q", "clubs")]) + new_game.payout(new_game.player.hands[0], new_game.dealer.hand) + assert new_game.player.money == 110 + + new_game.create_hands(10) + new_game.player.hands[0] = Hand(10, [Card("A", "spades"), + Card("J", "clubs")]) + new_game.payout_blackjack(new_game.player.hands[0]) + assert new_game.player.money == 125 + + new_game.create_hands(10) + assert new_game.player.money == 115 + new_game.player.hands[0] = Hand(10, [Card("2", "spades"), + Card("J", "clubs")]) + new_game.dealer.hand = Hand(0, [Card("A", "spades"), + Card("J", "clubs")]) + new_game.player.insured = True + assert new_game.dealer.hand.get_value() == 21 + new_game.payout(new_game.player.hands[0], new_game.dealer.hand) + assert new_game.player.money == 125 + + +def test_get_available_actions(): + options = GameOptions() + new_game = Game(options, "Alan") + new_game.player.hands.append(Hand(10, [Card("2", "spades"), + Card("J", "clubs")])) + new_game.dealer.hand = Hand(0, [Card("J", "spades"), Card("7", "Clubs")]) + actions = new_game.get_available_actions(new_game.player.hands[0]) + assert actions["hit"] + assert actions["double"] + assert not actions["split"] + assert actions["surrender"] + new_game.player.hands[0] = Hand(10, [Card("J", "spades"), + Card("J", "clubs")]) + new_game.dealer.hand = Hand(0, [Card("A", "spades"), Card("7", "Clubs")]) + actions = new_game.get_available_actions(new_game.player.hands[0]) + + assert actions["hit"] + assert actions["double"] + assert actions["split"] + assert actions["surrender"] diff --git a/blackjack/tests/test_hand.py b/blackjack/tests/test_hand.py new file mode 100644 index 0000000..94804c8 --- /dev/null +++ b/blackjack/tests/test_hand.py @@ -0,0 +1,68 @@ +from blackjack.hand import Hand +from blackjack.card import Card + + +def test_card_list_holds_cards(): + test_cards = [] + test_cards.append(Card("10", "diamonds")) + test_cards.append(Card("J", "clubs")) + + hand = Hand(10, test_cards) + assert hand.cards + + +def test_get_hand_value(): + test_cards = [] + test_cards.append(Card("10", "diamonds")) + test_cards.append(Card("J", "clubs")) + + hand = Hand(10, test_cards) + assert hand.get_value() == 20 + + +def test_get_ranks(): + test_cards = [] + test_cards.append(Card("10", "diamonds")) + test_cards.append(Card("J", "clubs")) + + hand = Hand(10, test_cards) + assert hand.get_ranks() == ["10", "J"] + + +def test_ace_detection_and_swap(): + test_cards = [] + test_cards.append(Card("10", "diamonds")) + test_cards.append(Card("J", "clubs")) + test_cards.append(Card("A", "hearts")) + + hand = Hand(10, test_cards) + assert hand.get_value() == 21 + + +def test_two_aces(): + test_cards = [] + test_cards.append(Card("A", "diamonds")) + test_cards.append(Card("A", "hearts")) + + hand = Hand(10, test_cards) + + assert hand.get_value() == 12 + + +def test_ace_value_incorrect(): + test_cards = [] + test_cards.append(Card("A", "spades")) + test_cards.append(Card("9", "clubs")) + test_cards[0].swap_ace() + hand = Hand(10, test_cards) + + assert hand.get_value() == 20 + + +def test_blackjack(): + test_cards = [] + test_cards.append(Card("A", "spades")) + test_cards.append(Card("10", "clubs")) + hand = Hand(10, test_cards) + + assert hand.get_value() == 21 diff --git a/blackjack/tests/test_player.py b/blackjack/tests/test_player.py new file mode 100644 index 0000000..e8bf1df --- /dev/null +++ b/blackjack/tests/test_player.py @@ -0,0 +1,29 @@ +from blackjack.player import Player + + +def test_player_creation(): + player = Player("Alan") + assert player + + +def test_player_attributes(): + player = Player("Alan") + assert player.name == "Alan" + assert player.money == 100 + assert player.hands == [] + + +def test_modify_money(): + player = Player("Alan") + player.modify_money(100) + assert player.money == 200 + player.modify_money(-50) + assert player.money == (150) + + +def test_modify_money_exception(): + player = Player("Alan") + try: + player.modify_money(-101) + except ValueError: + assert True diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..8a08d48 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +pytest=2.6.4