Skip to contents

Introduction

The Blackjack package was designed to be a clean, extensible, and robust simulation of the game of Blackjack for R users. This vignette documents key architectural and design decisions, including function structure, argument choices, workflow, modularity, and use of R’s object-oriented features.

Internal Modular Functions

Function Names and Arguments

The function names are chosen to be descriptive and follow common naming conventions, making them easy to understand for users with different levels of experience in programming. Each function name reflects the operation it performs in a straightforward manner.

The arguments for the functions are designed to be intuitive, minimizing the need for extensive documentation. The names of the arguments are chosen to be as self-explanatory as possible: - num_players and num_decks in play_blackjack() are used to define the number of players and decks, respectively. These arguments are crucial for customizing the game based on the user’s preferences.

  • dealer_hand and player_hands in dealer_play() and deal_cards() represent the hands of the dealer and the players, ensuring clarity when referencing the cards being dealt.

  • player_accepts in insurance_bet() makes it clear whether the player has accepted the insurance bet, reflecting the player’s choice.

Usage of Non-standard evaluation and C++

Non-standard evaluation (NSE)

NSE is used to capture and evaluate user input dynamically (e.g., “Hit” or “Stand”), allowing for flexible game flow and extending the package’s functionality. Modular functions, such as player_split() and dealer_play(), ensure that each game aspect is independent and easy to extend, making the codebase adaptable to new rules or features.

C++ Integration for Performance Enhancement

The Blackjack package integrates C++ through the use of Rcpp to efficiently handle card scoring. Specifically, the cpp_score_hand function calculates the total score of a player’s hand, iterating over the cards, mapping their ranks to values, and adjusting for Aces (counting them as either 1 or 11). This integration allows the package to offload computation-heavy tasks to C++, significantly improving the performance, especially when dealing with multiple players or simulations. By using a std::map to store card values and handling Aces dynamically, the C++ code is optimized for speed, making the game flow faster and smoother.

The C++ function is seamlessly exposed to R, allowing users to invoke it directly through Rcpp’s export functionality. This integration minimizes the overhead of R’s native loops, ensuring that the hand-scoring operation is efficient even in computationally intense scenarios. Additionally, the R interface ensures that errors, such as invalid card ranks, are properly handled, providing a robust experience for users. The C++ integration thus serves as a performance enhancement, enabling the package to handle larger datasets or more rounds without compromising on responsiveness. Future enhancements could include additional optimizations or a deeper integration of C++ for other aspects of the game.

Generic functions

  • Single-purpose functions: Each exported function has one clear job (dealing, scoring, splitting, insurance, etc.), supporting easier testing and extension.
  • Composability: Outputs are lists or atomic values, so functions can be chained or combined.
  • Explicit game workflow: The play_blackjack() function orchestrates setup, player turns, dealer play, and scoring, reflecting the real rules of Blackjack.

Each function is modular, with a clear and specific responsibility. This structure allows easy testing, modification, and extension of the package:

Object-Oriented Design (S3/vctrs)

  • Custom vctrs class: Cards are represented by a custom card_vector S3/vctrs class. This enables:
    • Type safety for all card-related operations
    • Attractive printing or font (e.g., [A♠])
    • Integration with tidyverse/vctrs tools
  • Supporting methods: Implemented format(), vec_ptype2(), and vec_cast() methods for compatibility and usability.
  • Extractors: Generic-style functions card_rank(), card_suit(), card_is_face() for robust, reusable card information extraction.

Card Vector Functions (like card_vector, card_rank, card_suit, card_is_face) handle the representation of cards and provide methods to extract or check information about them.

  • card_vector(): Creates a custom vector class (card_vector) for representing a deck of cards as strings, such as “A♠”, “10♣”, “K♥”. Usage: Takes a character vector and returns a card_vector object.

  • card_rank(): Extracts the rank (number or face value) from each card in the card_vector (e.g., “A”, “10”, “Q”). Usage: Returns a character vector with ranks extracted from the card_vector.

  • card_suit(): Extracts the suit (e.g., “♠”, “♣”, “♥”) from each card in the card_vector. Usage: Returns a character vector with suits extracted from the card_vector.

  • card_is_face(): Checks if each card in the card_vector is a face card (Jack, Queen, or King). Usage: Returns a logical vector indicating whether the card is a face card (TRUE for face cards, FALSE for others).

Game Flow Functions

These functions manage the overall game flow, starting the game, dealing cards, and managing the player’s and dealer’s turns.

  • create_board(): Generates a shuffled deck of cards with a specified number of decks and returns a shuffled deck as a character vector of card strings (e.g., “A♠”, “10♣”, “K♥”).

  • deal_cards(): Deals 2 cards to each player and 1 card to the dealer from a shuffled deck and returns a list containing player_hands, dealer_hand, and the remaining deck.

  • dealer_play(): Simulates the dealer’s turn, drawing cards until the dealer’s hand reaches 17 or the dealer gets 5 cards without busting, and returns the updated dealer’s hand and the remaining deck.

  • player_split(): Allows the player to split a pair of cards into two separate hands and returns whether the player can split the cards and the resulting split hands.

  • play_blackjack(): Starts an interactive game of Blackjack, prompting users for the number of players, decks, and actions like hit, stand, surrender, and double down, and runs the game loop managing player actions and dealer behavior.

Betting Functions

These functions manage the player’s betting actions, such as doubling down, taking insurance, or surrendering.

  • insurance_bet(): Offers the player the option to take insurance if the dealer’s upcard is Ace. If accepted, it checks if the dealer has Blackjack and calculates the payout, returning the result of the insurance bet, including the payout and whether the insurance was successful.

  • double_down(): Allows a player to double their bet by taking exactly one more card and returns the updated hand, the new deck, and a validity flag (TRUE if double down is valid).

  • surrender_hand(): Allows a player to surrender and forfeit half their bet and returns a status (whether the player has surrendered or not) and the associated message regarding the surrender.

Scoring Functions

These functions calculate the score of the player’s hand and manage the game’s rules for scoring.

  • score_hand(): Calculates the score of a hand, adjusting for Aces (if score > 21, Ace is treated as 1) and returns the score of the hand, considering the Ace as 1 if necessary.

  • announce_winner(): Compares the player scores with the dealer’s score and announces the outcome (Win, Lose, or Tie), returning a vector of outcomes for each player.

User Workflow

  • Interactive and scriptable: Users can either step through the game interactively (play_blackjack()) or call lower-level functions for custom simulations or analyses.
  • Defensive programming: All functions check argument types/lengths and handle edge cases (e.g., invalid hands, out-of-bounds splits).
  • Clear feedback: Results are printed in a readable, game-like format (e.g., final hands, scores, win/lose messages).

The game flow is designed to be interactive, with the user making decisions at each stage. The play_blackjack function guides the user through the game, prompting them to decide whether to surrender, split, or double down. The use of the readline() function allows for this interactive user experience. After each action, the game continues, and the player’s hand is updated accordingly.

The workflow is as follows:

  • Initial Setup:The user is prompted to enter the number of players and decks. The function validates the number of players and ensures a valid number of decks are selected, defaulting based on the number of players if needed.

  • Deck Creation and Card Dealing: A shuffled deck is created using the create_board() function, and the deal_cards() function distributes 2 cards to each player and 1 card to the dealer. The dealer’s first card is revealed, and the remaining cards are made available for players’ actions.

  • Insurance Option: If the dealer’s upcard is an Ace, players are offered the option to take insurance. The outcome of the insurance bet is evaluated, including the check for a Blackjack on the dealer’s part, and the results are recorded.

  • Player Actions: For each player, the game proceeds with a series of interactive prompts:

  • Surrender: Players can opt to surrender, forfeiting half of their bet.

  • Split: If a player has a pair, they can choose to split it into two separate hands.

  • Double Down: Players with hands valued at 10 or 11 can choose to double down by adding a single card to their hand.

  • Hit/Stand: Players may continue to hit (receive additional cards) or stand (end their turn). The game ensures that invalid inputs are handled appropriately.

  • Dealer’s Turn: After all players have finished their turns, the dealer plays according to the Blackjack rules, drawing cards until reaching a score of at least 17 or obtaining 5 cards without busting. The dealer’s hand is revealed and the final score is calculated.

  • Result Calculation: Once the dealer completes their turn, the scores of the players are compared against the dealer’s score. Each player’s outcome is determined: Win, Lose, or Tie based on their final score relative to the dealer’s hand.

  • Round Summary and Replay: The results for each player are displayed, and the game provides a summary of the final hands and scores. Players are invited to start a new round or end the session, with the option to adjust the number of decks or players for subsequent rounds.

Limitations

Test Coverage: The current tests focus on core mechanics but lack edge case handling (e.g., invalid inputs). Future work should improve test coverage to handle more complex scenarios.

Gameplay Features: Advanced betting strategies (e.g., card counting, progressive betting) and multi-round tracking are not implemented. These features could be added for a richer experience.

UI/UX: The text-based interface works but isn’t ideal for larger groups. A GUI using shiny or ggplot2 would improve user experience.

Multiplayer Functionality: Players are prompted sequentially. Supporting asynchronous input for larger groups could enhance gameplay.

Alternative designs

Object-Oriented Design (OOP): An OOP approach was considered but not implemented, as it would complicate the code unnecessarily. A function-based design was chosen for simplicity and flexibility.

Interactive GUI: A GUI was considered but not implemented due to time constraints. A text-based interface was used for simplicity, though a GUI could be added in the future.

Real-Time Multiplayer: Real-time multiplayer via WebSockets was considered but not implemented to keep the project simpler. A sequential turn-based model was chosen for ease of use.

Modular Betting Strategies: Complex betting strategies (e.g., Martingale) were deferred to keep the game focused on basic actions. They could be added later for advanced users.

AI vs. human contributions

The development of this package has been a collaborative effort involving both human expertise and AI assistance.

Humane contributions

The development of this package was primarily driven by human expertise in crafting the core game logic and functionality. The developer designed and implemented the essential game functions, including play_blackjack(), dealer_play(), double_down(), and others that manage gameplay mechanics such as card dealing, player actions, and the determination of game outcomes. The developer also made critical design decisions, such as using a function-based approach over object-oriented programming, which allowed for clearer modularization and flexibility. Additionally, the human contributions encompassed writing and refining the package’s documentation, ensuring clear explanations of the functions, their purposes, and usage examples. The testing was also handled by the developer, who focused on edge cases and ensuring the integrity of the game logic.

AI contributions

AI contributed significantly to the debugging and optimization phases of this project. It assisted in identifying and resolving errors encountered during test execution, particularly when running devtools::check() and resolving issues with the package loading process. Additionally, AI played a key role in optimizing the efficiency of functions, particularly when dealing with large datasets or complex operations such as scoring hands in the Blackjack game. By suggesting more efficient code structures and practices, it helped streamline the game’s logic and reduce redundant operations. Moreover, AI provided valuable guidance on integrating C++ functions, particularly in optimizing the score_hand_dynamic function. It assisted in resolving issues with the C++ code compilation and helped re-factor the function to handle card rank extraction and score calculation more efficiently, improving performance in scenarios involving large hands or multiple players. Through these contributions, AI helped enhance the functionality, performance, and overall robustness of the package.