diff --git a/project/UML.png b/project/UML.png new file mode 100644 index 00000000..930535d7 Binary files /dev/null and b/project/UML.png differ diff --git a/project/example/example.py b/project/example/example.py new file mode 100644 index 00000000..b8b1c68b --- /dev/null +++ b/project/example/example.py @@ -0,0 +1,12 @@ +from project.scr.persons import BlackjackPlayer +from project.scr.strategies import ConservativeStrategy +from project.scr.game import BlackjackGame + +# Минимальный рабочий пример с одной стратегией +players = [ + BlackjackPlayer(strategy=ConservativeStrategy(), initial_bankroll=100), + BlackjackPlayer(initial_bankroll=100), # Использует Conservative по умолчанию +] + +game = BlackjackGame(players=players, num_decks=1) # 1 колода для простоты +game.play_full_round() diff --git a/project/example/example.txt b/project/example/example.txt new file mode 100644 index 00000000..74f1b645 --- /dev/null +++ b/project/example/example.txt @@ -0,0 +1,82 @@ +=== BLACKJACK GAME SESSION === + +[ ROUND 1 INITIATED ] + +• WAGERS PLACED: + - Player 1: $10 + - Player 2: $10 + - Player 3: $20 (high roller) + - Player 4: $10 + +• DEALER'S INITIAL MOVE: + Visible Card: ♦4 + (Hidden card dealt) + +• PLAYER ACTIONS: + +Player #1: +Cards: ♣5 ♥9 (Total: 14) +Moves: Drew card, Drew card, Stand + +Player #2: +Cards: ♠4 ♦6 ♥3 ♣8 (Total: 21) +Moves: Drew card, Drew card, Doubled bet, Drew card, Drew card + +Player #3: +Cards: ♠10 ♣9 ♠3 (BUST) +Moves: Drew card, Drew card, Drew card + +Player #4: +Cards: ♠8 ♣Q (Total: 18) +Moves: Drew card, Drew card, Stand + +• DEALER'S TURN: +Revealed Hand: ♦4 ♣3 (Total: 7) +Final Hand: ♦4 ♣3 ♥6 ♠2 ♥5 (Total: 20) + +• ROUND RESULTS: + +Player #1: LOSS (14 vs 20) +Player #2: WIN (21) +Player #3: LOSS (Bust) +Player #4: LOSS (18 vs 20) + +[ ROUND 1 CONCLUDED ] + +[ ROUND 2 INITIATED ] + +• WAGERS PLACED: + - Player 1: $10 + - Player 2: $10 + - Player 3: $20 + - Player 4: Cannot bet (insufficient funds) + +• DEALER'S INITIAL MOVE: + Visible Card: ♠6 + (Hidden card dealt) + +• PLAYER ACTIONS: + +Player #1: +Cards: ♦Q ♥10 (Total: 20) +Moves: Drew card, Drew card, Stand + +Player #2: +Cards: ♠A ♠5 ♥7 ♣4 (Total: 17) +Moves: Drew card, Drew card, Drew card, Drew card, Stand + +Player #3: +Cards: ♠K ♦7 ♠3 (Total: 20) +Moves: Drew card, Drew card, Drew card, Stand + +• DEALER'S TURN: +Revealed Hand: ♠6 ♥5 (Total: 11) +Final Hand: ♠6 ♥5 ♣6 (Total: 17) + +• ROUND RESULTS: + +Player #1: WIN (20 vs 17) +Player #2: PUSH (17 vs 17) +Player #3: WIN (20 vs 17) + +[ ROUND 2 CONCLUDED ] diff --git a/project/scr/desk.py b/project/scr/desk.py new file mode 100644 index 00000000..7a3ef785 --- /dev/null +++ b/project/scr/desk.py @@ -0,0 +1,146 @@ +from typing import List, Dict +from project.scr.persons import BlackjackPlayer, BlackjackDealer +from project.scr.objects import PlayerHand, GameResult + + +class BlackjackTable: + """ + Manages the blackjack game flow, player actions, and dealer interactions. + + Attributes: + players (List[BlackjackPlayer]): List of active players + dealer (BlackjackDealer): The game dealer + player_hands (Dict[BlackjackPlayer, List[PlayerHand]]): Active hands per player + """ + + def __init__(self, players: List[BlackjackPlayer], num_decks: int = 6): + """Initialize a new blackjack table""" + self.players = players + self.dealer = BlackjackDealer(num_decks) + self.player_hands: Dict[BlackjackPlayer, List[PlayerHand]] = {} + self._initialize_table() + + def _initialize_table(self) -> None: + """Prepare the table for a new round""" + self.dealer.new_round() + for player in self.players: + player.clear_hands() + new_hand = PlayerHand([]) + player.add_hand(new_hand) + self.player_hands[player] = [new_hand] + + def deal_card_to_hand(self, hand: PlayerHand, face_up: bool = True) -> None: + """ + Deal a card from the dealer's shoe to a specific hand + + Args: + hand: The target hand to receive the card + face_up: Whether the card should be dealt face up + """ + card = self.dealer.deal_card(face_up) + hand.add_card(card) + + def place_initial_bets(self) -> None: + """Handle initial betting round for all players""" + for player in self.players: + if player.can_place_bet(player.strategy.min_bet): + bet_amount = player.strategy.min_bet + player.place_bet(bet_amount) + # Initialize bet for first hand + if self.player_hands[player]: + self.player_hands[player][0].wager = bet_amount + else: + # Mark player as out if can't place minimum bet + if self.player_hands[player]: + self.player_hands[player][0].status = GameResult.LOSE + + def split_hand(self, player: BlackjackPlayer, hand_index: int = 0) -> None: + """ + Split a player's hand into two separate hands + + Args: + player: The player requesting the split + hand_index: Index of the hand to split + """ + if hand_index >= len(self.player_hands[player]): + return + + original_hand = self.player_hands[player][hand_index] + + # Verify split is possible + if ( + len(original_hand.cards) != 2 + or original_hand.cards[0].rank != original_hand.cards[1].rank + ): + return + + # Create two new hands from the split + new_hand1, new_hand2 = original_hand.split_hand() + + # Place additional bet + if player.can_place_bet(original_hand.wager): + player.place_bet(original_hand.wager) + else: + return + + # Deal cards to new hands + self.deal_card_to_hand(new_hand1) + self.deal_card_to_hand(new_hand2) + + # Replace original hand with split hands + self.player_hands[player].pop(hand_index) + self.player_hands[player].extend([new_hand1, new_hand2]) + player.active_hands = self.player_hands[player] + + def start_new_round(self) -> None: + """Reset the table for a new round of play""" + self._initialize_table() + + # Deal initial cards: 2 to each player, 1 to dealer + 1 face down + for _ in range(2): + for player in self.players: + for hand in self.player_hands[player]: + self.deal_card_to_hand(hand) + + # Dealer gets one face up and one face down card + self.deal_card_to_hand(self.dealer.hand) + self.deal_card_to_hand(self.dealer.hand, face_up=False) + + def player_action( + self, player: BlackjackPlayer, action: str, hand_index: int = 0 + ) -> None: + """ + Process player action for specific hand + + Args: + player: The player taking action + action: Action to perform (hit, stand, double, split, etc.) + hand_index: Index of the hand to act upon + """ + if hand_index >= len(self.player_hands[player]): + return + + current_hand = self.player_hands[player][hand_index] + + match action: + case "hit": + self.deal_card_to_hand(current_hand) + if current_hand.calculate_value() == -1: # Bust + current_hand.status = GameResult.BUST + + case "stand": + current_hand.status = GameResult.ACTIVE + + case "double": + if player.can_place_bet(current_hand.wager): + player.place_bet(current_hand.wager) + current_hand.double_wager() + self.deal_card_to_hand(current_hand) + current_hand.status = GameResult.ACTIVE + + case "split": + self.split_hand(player, hand_index) + + case "surrender": + player.adjust_bankroll(current_hand.wager // 2) + current_hand.surrender() diff --git a/project/scr/game.py b/project/scr/game.py new file mode 100644 index 00000000..5e6a7964 --- /dev/null +++ b/project/scr/game.py @@ -0,0 +1,211 @@ +from enum import Enum, auto +from typing import List, Dict +from project.scr.objects import PlayerHand, PlayingCard, GameResult +from project.scr.persons import BlackjackPlayer, BlackjackDealer +from project.scr.strategies import PlayerAction, BaseStrategy +from project.scr.desk import BlackjackTable + + +class RoundPhase(Enum): + """Represents different phases of a blackjack round""" + + INITIALIZATION = auto() + BETTING = auto() + INITIAL_DEAL = auto() + PLAYER_TURNS = auto() + DEALER_TURN = auto() + PAYOUTS = auto() + COMPLETION = auto() + + +class BlackjackGame: + """ + Controls the flow of a blackjack game with multiple players and rounds. + + Attributes: + table (BlackjackTable): The game table with players and dealer + current_phase (RoundPhase): Current game phase + round_number (int): Current round count + """ + + def __init__(self, players: List[BlackjackPlayer], num_decks: int = 6): + """Initialize a new blackjack game session""" + self.table = BlackjackTable(players, num_decks) + self.current_phase = RoundPhase.INITIALIZATION + self.round_number = 0 + self._setup_observers() + + def _setup_observers(self) -> None: + """Setup game state observers for UI updates""" + self.observers: list = [] + + def _notify_observers(self) -> None: + """Notify all observers about game state changes""" + for observer in self.observers: + observer.update(self) + + def start_new_round(self) -> None: + """Begin a new round of blackjack""" + self.round_number += 1 + self.current_phase = RoundPhase.INITIALIZATION + self.table.start_new_round() + self._notify_observers() + + def process_betting_phase(self) -> None: + """Handle the betting phase of the round""" + self.current_phase = RoundPhase.BETTING + self.table.place_initial_bets() + self._notify_observers() + + def process_initial_deal(self) -> None: + """Perform the initial card deal""" + self.current_phase = RoundPhase.INITIAL_DEAL + self._notify_observers() + + def process_player_turns(self) -> None: + """Handle all player decisions""" + self.current_phase = RoundPhase.PLAYER_TURNS + dealer_upcard = self.table.dealer.hand.cards[0] + + for player in self.table.players: + for i, hand in enumerate(self.table.player_hands[player]): + if hand.status != GameResult.ACTIVE: + continue + + while True: + action = player.strategy.decide_action(hand, dealer_upcard) + + if action == PlayerAction.STAND: + break + + elif action == PlayerAction.HIT: + self.table.deal_card_to_hand(hand) + if hand.calculate_value() == -1: # Bust + hand.status = GameResult.BUST + break + + elif action == PlayerAction.DOUBLE: + if len(hand.cards) == 2 and player.can_place_bet(hand.wager): + player.place_bet(hand.wager) + hand.double_wager() + self.table.deal_card_to_hand(hand) + break + + elif action == PlayerAction.SPLIT: + if ( + len(hand.cards) == 2 + and hand.cards[0].rank == hand.cards[1].rank + ): + self.table.split_hand(player, i) + # Need to re-evaluate after split + continue + + elif action == PlayerAction.SURRENDER: + if len(hand.cards) == 2: + player.adjust_bankroll(hand.wager // 2) + hand.status = GameResult.LOSE + break + self._notify_observers() + + def process_dealer_turn(self) -> None: + """Handle the dealer's play""" + self.current_phase = RoundPhase.DEALER_TURN + dealer_hand = self.table.dealer.hand + + # Dealer draws according to rules + while dealer_hand.calculate_value() not in (-1, 17, 18, 19, 20, 21): + self.table.deal_card_to_hand(dealer_hand) + + dealer_hand.status = ( + GameResult.WIN if dealer_hand.calculate_value() != -1 else GameResult.BUST + ) + self._notify_observers() + + def process_payouts(self) -> None: + """Calculate and distribute winnings""" + self.current_phase = RoundPhase.PAYOUTS + dealer_value = self.table.dealer.hand.calculate_value() + dealer_busted = dealer_value == -1 + + for player in self.table.players: + for hand in self.table.player_hands[player]: + if hand.status != GameResult.ACTIVE: + continue + + player_value = hand.calculate_value() + + if player_value == -1: # Player busted + hand.status = GameResult.BUST + + elif len(hand.cards) == 2 and player_value == 21: # Blackjack + hand.status = GameResult.BLACKJACK + player.adjust_bankroll(int(hand.wager * 2.5)) + + elif dealer_busted or player_value > dealer_value: + hand.status = GameResult.WIN + player.adjust_bankroll(hand.wager * 2) + + elif player_value == dealer_value: + hand.status = GameResult.PUSH + player.adjust_bankroll(hand.wager) + + else: + hand.status = GameResult.LOSE + self._notify_observers() + + def complete_round(self) -> None: + """Clean up after round completion""" + self.current_phase = RoundPhase.COMPLETION + self._notify_observers() + + def play_full_round(self) -> None: + """Execute a complete round from start to finish""" + self.start_new_round() + self.process_betting_phase() + self.process_initial_deal() + self.process_player_turns() + self.process_dealer_turn() + self.process_payouts() + self.complete_round() + + def add_observer(self, observer) -> None: + """Add a game state observer""" + self.observers.append(observer) + + +class GameObserver: + """Base class for game state observers""" + + def update(self, game: BlackjackGame) -> None: + """Handle game state updates""" + self._display_round_info(game) + self._display_phase_info(game.current_phase) + self._display_table_state(game.table) + + def _display_round_info(self, game: BlackjackGame) -> None: + print(f"\n=== Round {game.round_number} ===") + + def _display_phase_info(self, phase: RoundPhase) -> None: + phase_descriptions = { + RoundPhase.INITIALIZATION: "Setting up new round...", + RoundPhase.BETTING: "Players placing bets...", + RoundPhase.INITIAL_DEAL: "Dealing initial cards...", + RoundPhase.PLAYER_TURNS: "Players making decisions...", + RoundPhase.DEALER_TURN: "Dealer playing hand...", + RoundPhase.PAYOUTS: "Calculating results...", + RoundPhase.COMPLETION: "Round complete!", + } + print(phase_descriptions.get(phase, "")) + + def _display_table_state(self, table: BlackjackTable) -> None: + # Display dealer's hand + print("\nDealer's Hand:") + table.dealer.hand.display() + + # Display each player's hands + for i, player in enumerate(table.players): + print(f"\nPlayer {i + 1} (Bankroll: {player._bankroll}):") + for j, hand in enumerate(table.player_hands[player]): + print(f" Hand {j + 1} (Bet: {hand.wager}):") + hand.display() + print(f" Status: {hand.status.name}") diff --git a/project/scr/objects.py b/project/scr/objects.py new file mode 100644 index 00000000..e958922e --- /dev/null +++ b/project/scr/objects.py @@ -0,0 +1,136 @@ +from random import SystemRandom +from itertools import product +from enum import Enum, auto +from typing import List, Dict, Tuple, Optional, Union +from dataclasses import dataclass + + +class CardSuit(Enum): + CLUBS = auto() + DIAMONDS = auto() + HEARTS = auto() + SPADES = auto() + + def __str__(self): + return { + CardSuit.CLUBS: "♣", + CardSuit.DIAMONDS: "♦", + CardSuit.HEARTS: "♥", + CardSuit.SPADES: "♠", + }[self] + + +class PlayingCard: + """Represents a single playing card with suit and rank""" + + def __init__(self, suit: CardSuit, rank: Union[int, str]): + self._suit = suit + self._rank = str(rank).upper() + + @property + def suit(self) -> CardSuit: + return self._suit + + @property + def rank(self) -> str: + return self._rank + + def __repr__(self) -> str: + return f"{self.rank}{str(self.suit)}" + + def __str__(self) -> str: + return f"{self.rank} of {self.suit.name.capitalize()}" + + +class CardDeck: + """Implements a standard 52-card deck with advanced features""" + + def __init__(self, num_decks: int = 1): + self._rng = SystemRandom() + self._cards = self._initialize_deck(num_decks) + self.shuffle_cards() + + def _initialize_deck(self, num_decks: int) -> List[PlayingCard]: + ranks = [str(n) for n in range(2, 11)] + ["J", "Q", "K", "A"] + return [ + PlayingCard(suit, rank) + for _ in range(num_decks) + for suit, rank in product(CardSuit, ranks) + ] + + def shuffle_cards(self) -> None: + """Performs cryptographic-quality shuffle""" + self._rng.shuffle(self._cards) + + def deal_card(self) -> PlayingCard: + """Removes and returns the top card from deck""" + if not self._cards: + raise ValueError("No cards remaining in deck") + return self._cards.pop() + + @property + def remaining(self) -> int: + return len(self._cards) + + +class GameResult(Enum): + ACTIVE = auto() + WIN = auto() + LOSE = auto() + BLACKJACK = auto() + PUSH = auto() + BUST = auto() + + +@dataclass +class PlayerHand: + cards: List[PlayingCard] + wager: int = 0 + is_active: bool = True + status: GameResult = GameResult.ACTIVE + + def add_card(self, card: PlayingCard) -> None: + self.cards.append(card) + + def calculate_value(self) -> int: + total = 0 + aces = 0 + + for card in self.cards: + if card.rank.isdigit(): + total += int(card.rank) + elif card.rank in ("J", "Q", "K"): + total += 10 + else: # Ace + total += 11 + aces += 1 + + while total > 21 and aces: + total -= 10 + aces -= 1 + + return total if total <= 21 else -1 + + def is_blackjack(self) -> bool: + return len(self.cards) == 2 and self.calculate_value() == 21 + + def double_wager(self) -> None: + self.wager *= 2 + + def surrender(self) -> None: + self.is_active = False + self.status = GameResult.LOSE + + def split_hand(self) -> Tuple["PlayerHand", "PlayerHand"]: + if len(self.cards) != 2: + raise ValueError("Can only split with exactly two cards") + + hand1 = PlayerHand([self.cards[0]], self.wager) + hand2 = PlayerHand([self.cards[1]], self.wager) + return hand1, hand2 + + def display(self) -> None: + print("Current hand:") + for card in self.cards: + print(f" {card}") + print(f"Total value: {self.calculate_value()}") diff --git a/project/scr/persons.py b/project/scr/persons.py new file mode 100644 index 00000000..ecccf605 --- /dev/null +++ b/project/scr/persons.py @@ -0,0 +1,101 @@ +from typing import List +import random +from project.scr.objects import CardDeck, PlayerHand, PlayingCard +from project.scr.strategies import BaseStrategy, ConservativeStrategy + + +class BlackjackPlayer: + """ + Represents a player in blackjack game with betting capabilities and strategy. + + Attributes: + strategy (BaseStrategy): The playing strategy to use + bankroll (int): Current chip count + current_bet (int): Active wager amount + """ + + def __init__(self, strategy=None, initial_bankroll: int = 1000): + self.strategy = strategy or ConservativeStrategy() + self._bankroll = initial_bankroll + self.current_bet = 0 + self.active_hands: List[PlayerHand] = [] + + def adjust_bankroll(self, amount: int) -> None: + """Modify player's chip count by specified amount""" + self._bankroll += amount + + def can_place_bet(self, amount: int) -> bool: + """Check if player has sufficient funds for a bet""" + return 0 < amount <= self._bankroll + + def place_bet(self, amount: int) -> bool: + """Attempt to place a wager, returns True if successful""" + if self.can_place_bet(amount): + self.current_bet = amount + self._bankroll -= amount + return True + return False + + def clear_hands(self) -> None: + """Reset all active hands for new round""" + self.active_hands = [] + + def add_hand(self, hand: PlayerHand) -> None: + """Add a new hand to player's active hands""" + self.active_hands.append(hand) + + +class BlackjackDealer: + """ + Manages the dealer's operations including card dealing and game flow. + + Attributes: + shoe (List[CardDeck]): Collection of card decks in play + hand (PlayerHand): Dealer's current hand + """ + + def __init__(self, deck_count: int = 6): + self.shoe = self._initialize_shoe(deck_count) + # self.hand: PlayerHand = None + + def _initialize_shoe(self, deck_count: int) -> List[CardDeck]: + """Create and shuffle multiple decks for the shoe""" + decks = [CardDeck() for _ in range(deck_count)] + for deck in decks: + deck.shuffle_cards() + return decks + + def deal_card(self, face_up: bool = True) -> PlayingCard: + """ + Deal a card from random deck in shoe + + Args: + face_up: Whether card should be dealt face up + + Returns: + PlayingCard: The dealt card + """ + active_deck = random.choice(self.shoe) + try: + card = active_deck.deal_card() + # card.face_up = face_up + return card + except ValueError: + # Reshuffle if deck is empty + self.shoe = self._initialize_shoe(len(self.shoe)) + return self.deal_card(face_up) + + def reveal_hand(self) -> None: + """Show dealer's hand with all cards face up""" + if self.hand: + # for card in self.hand.cards: + # card.face_up = True + print("\nDealer's Hand:") + self.hand.display() + + def new_round(self) -> None: + """Prepare dealer for new game round""" + self.hand = PlayerHand([]) + # Check if shoe needs replenishing + if sum(deck.remaining for deck in self.shoe) < 52: + self.shoe = self._initialize_shoe(len(self.shoe)) diff --git a/project/scr/strategies.py b/project/scr/strategies.py new file mode 100644 index 00000000..a334db35 --- /dev/null +++ b/project/scr/strategies.py @@ -0,0 +1,166 @@ +from enum import Enum, auto +from typing import Optional +from dataclasses import dataclass +from project.scr.objects import PlayerHand, PlayingCard + + +class PlayerAction(Enum): + """Defines possible actions a player can take in blackjack""" + + STAND = auto() + HIT = auto() + SPLIT = auto() + DOUBLE = auto() + TRIPLE = auto() + SURRENDER = auto() + + +@dataclass +class BaseStrategy: + """Abstract base class for all blackjack strategies""" + + min_bet: int = 10 + use_insurance: bool = False + + def decide_action( + self, hand: PlayerHand, dealer_upcard: PlayingCard + ) -> PlayerAction: + """Determine optimal action based on game state""" + raise NotImplementedError("Strategy subclass must implement decide_action") + + +class ConservativeStrategy(BaseStrategy): + """Basic strategy with minimal risk""" + + def __init__(self): + super().__init__(use_insurance=True) + + def decide_action( + self, hand: PlayerHand, dealer_upcard: PlayingCard + ) -> PlayerAction: + hand_value = hand.calculate_value() + + if hand_value < 17: + return PlayerAction.HIT + return PlayerAction.STAND + + +class ScientificStrategy(BaseStrategy): + """Mathematically optimal strategy based on probability""" + + def decide_action( + self, hand: PlayerHand, dealer_upcard: PlayingCard + ) -> PlayerAction: + cards = hand.cards + hand_value = hand.calculate_value() + dealer_rank = dealer_upcard.rank + + # Check for split opportunities first + split_action = self._evaluate_split(cards, dealer_rank) + if split_action: + return split_action + + # Check for double down opportunities + double_action = self._evaluate_double(hand_value, dealer_rank) + if double_action and len(cards) == 2: + return double_action + + # Handle soft totals (ace counted as 11) + if self._is_soft_hand(cards): + return self._handle_soft_hand(hand_value - 10, dealer_rank) + + # Standard decision making + return self._handle_hard_hand(hand_value, dealer_rank) + + def _evaluate_split( + self, cards: list[PlayingCard], dealer_rank: str + ) -> Optional[PlayerAction]: + if len(cards) != 2 or cards[0].rank != cards[1].rank: + return None + + card_rank = cards[0].rank + if card_rank in ("A", "8"): + return PlayerAction.SPLIT if dealer_rank != "A" else PlayerAction.HIT + elif card_rank == "5": + return PlayerAction.DOUBLE + elif card_rank in ("2", "3", "7"): + return ( + PlayerAction.SPLIT + if dealer_rank in ("2", "3", "4", "5", "6", "7") + else PlayerAction.HIT + ) + elif card_rank in ("4", "6", "9"): + # Additional split logic for these ranks + pass + + return None + + def _evaluate_double( + self, hand_value: int, dealer_rank: str + ) -> Optional[PlayerAction]: + if hand_value == 9 and dealer_rank in ("2", "3", "4", "5", "6"): + return PlayerAction.DOUBLE + elif hand_value in (10, 11) and dealer_rank not in ("A", "10", "J", "Q", "K"): + return PlayerAction.DOUBLE + return None + + def _is_soft_hand(self, cards: list[PlayingCard]) -> bool: + return ( + any(card.rank == "A" for card in cards) + and sum( + 11 + if card.rank == "A" + else 10 + if card.rank in ("J", "Q", "K") + else int(card.rank) + for card in cards + ) + <= 21 + ) + + def _handle_soft_hand(self, hand_value: int, dealer_rank: str) -> PlayerAction: + if hand_value <= 7: + return ( + PlayerAction.HIT + if dealer_rank in ("9", "10", "J", "Q", "K", "A") + else PlayerAction.DOUBLE + ) + elif hand_value == 8: + return ( + PlayerAction.STAND + if dealer_rank in ("2", "3", "4", "5", "6") + else PlayerAction.HIT + ) + else: + return PlayerAction.STAND + + def _handle_hard_hand(self, hand_value: int, dealer_rank: str) -> PlayerAction: + if hand_value <= 11: + return PlayerAction.HIT + elif 12 <= hand_value <= 16: + return ( + PlayerAction.STAND + if dealer_rank in ("2", "3", "4", "5", "6") + else PlayerAction.HIT + ) + else: + return PlayerAction.STAND + + +class HighRiskStrategy(BaseStrategy): + """Aggressive betting strategy with higher risk/reward""" + + def __init__(self): + super().__init__(min_bet=20) + + def decide_action( + self, hand: PlayerHand, dealer_upcard: PlayingCard + ) -> PlayerAction: + hand_value = hand.calculate_value() + + if hand_value <= 6: + return PlayerAction.DOUBLE + elif hand_value <= 15: + return PlayerAction.HIT + else: + return PlayerAction.STAND diff --git a/scripts/test_blackjak.py b/scripts/test_blackjak.py new file mode 100644 index 00000000..f29a5bcc --- /dev/null +++ b/scripts/test_blackjak.py @@ -0,0 +1,115 @@ +import pytest +from project.scr.persons import BlackjackPlayer +from project.scr.objects import PlayingCard, PlayerHand, GameResult, CardSuit +from project.scr.strategies import ScientificStrategy, HighRiskStrategy +from project.scr.game import BlackjackGame, RoundPhase +from project.scr.desk import BlackjackTable + +# Test card definitions +ACE = PlayingCard(CardSuit.SPADES, "A") +KING = PlayingCard(CardSuit.HEARTS, "K") +QUEEN = PlayingCard(CardSuit.DIAMONDS, "Q") +JACK = PlayingCard(CardSuit.CLUBS, "J") +TEN = PlayingCard(CardSuit.SPADES, "10") +NINE = PlayingCard(CardSuit.HEARTS, "9") +EIGHT = PlayingCard(CardSuit.DIAMONDS, "8") +SEVEN = PlayingCard(CardSuit.CLUBS, "7") +SIX = PlayingCard(CardSuit.SPADES, "6") +FIVE = PlayingCard(CardSuit.HEARTS, "5") +FOUR = PlayingCard(CardSuit.DIAMONDS, "4") +TWO = PlayingCard(CardSuit.CLUBS, "2") + + +@pytest.fixture +def test_players(): + """Fixture providing different player strategies for testing""" + return [ + BlackjackPlayer(strategy=ScientificStrategy(), initial_bankroll=1000), + BlackjackPlayer(strategy=HighRiskStrategy(), initial_bankroll=1000), + BlackjackPlayer(strategy=ScientificStrategy(), initial_bankroll=1000), + BlackjackPlayer(initial_bankroll=1000), + ] + + +def test_game_phases(test_players): + """Verify game progresses through all expected phases""" + game = BlackjackGame(test_players) + phases = [] + + game.start_new_round() + phases.append(game.current_phase) + + game.process_betting_phase() + phases.append(game.current_phase) + + game.process_initial_deal() + phases.append(game.current_phase) + + game.process_player_turns() + phases.append(game.current_phase) + + game.process_dealer_turn() + phases.append(game.current_phase) + + game.process_payouts() + phases.append(game.current_phase) + + game.complete_round() + phases.append(game.current_phase) + + assert phases == [ + RoundPhase.INITIALIZATION, + RoundPhase.BETTING, + RoundPhase.INITIAL_DEAL, + RoundPhase.PLAYER_TURNS, + RoundPhase.DEALER_TURN, + RoundPhase.PAYOUTS, + RoundPhase.COMPLETION, + ] + + +@pytest.mark.parametrize( + "cards,expected_score", + [ + ([ACE, KING], 21), + ([NINE, TEN], 19), + ([ACE, ACE, NINE], 21), + ([TEN, FIVE, SIX], 21), + ([TEN, TEN, FIVE], -1), + ([ACE, ACE, ACE, ACE], 14), + ([JACK, QUEEN], 20), + ], +) +def test_hand_scoring(cards, expected_score): + """Test hand score calculation with various card combinations""" + hand = PlayerHand(cards.copy()) + assert hand.calculate_value() == expected_score + + +def test_blackjack_detection(): + """Verify blackjack detection works correctly""" + blackjack = PlayerHand([ACE, KING]) + not_blackjack1 = PlayerHand([NINE, TEN, TWO]) + not_blackjack2 = PlayerHand([ACE, FIVE, FIVE]) + + assert blackjack.is_blackjack() is True + assert not_blackjack1.is_blackjack() is False + assert not_blackjack2.is_blackjack() is False + + +def test_bankroll_management(test_players): + """Test player bankroll changes correctly during game""" + game = BlackjackGame(test_players) + initial_bankrolls = [p._bankroll for p in test_players] + + # Play full round + game.play_full_round() + + # Verify bankroll changes + for i, player in enumerate(test_players): + if game.table.player_hands[player][0].status == GameResult.WIN: + assert player._bankroll > initial_bankrolls[i] + elif game.table.player_hands[player][0].status == GameResult.LOSE: + assert player._bankroll < initial_bankrolls[i] + elif game.table.player_hands[player][0].status == GameResult.PUSH: + assert player._bankroll == initial_bankrolls[i]