diff --git a/blackjack_game.py b/blackjack_game.py new file mode 100755 index 0000000..b964382 --- /dev/null +++ b/blackjack_game.py @@ -0,0 +1,66 @@ +from game_logic import deal_card, calculate_hand_total, has_blackjack + +class BlackjackGame: + def __init__(self): + self.player_hand = [] + self.dealer_card = 0 + self.player_total = 0 + self.has_blackjack = False + self.stats = { + "total_rounds": 0, + "correct_decisions": 0, + "incorrect_decisions": 0, + "by_scenario": { + "hard_totals": {"correct": 0, "incorrect": 0}, + "soft_totals": {"correct": 0, "incorrect": 0}, + "pairs": {"correct": 0, "incorrect": 0}, + }, + "current_streak": 0, + "longest_streak": 0, + "last_round_won": None, + "pending_streak_notification": None + } + + def update_stats(self, correct_decision, scenario_type): + """Update statistics based on the user's decision.""" + self.stats["total_rounds"] += 1 + + if correct_decision: + self.stats["correct_decisions"] += 1 + self.stats["by_scenario"][scenario_type]["correct"] += 1 + + if self.stats["last_round_won"] is None: # First decision + self.stats["current_streak"] = 1 + elif self.stats["last_round_won"]: # Continuing streak + self.stats["current_streak"] += 1 + else: # New streak after loss + self.stats["current_streak"] = 1 + + self.stats["last_round_won"] = True + self.stats["longest_streak"] = max(self.stats["longest_streak"], self.stats["current_streak"]) + + else: + self.stats["incorrect_decisions"] += 1 + self.stats["by_scenario"][scenario_type]["incorrect"] += 1 + + if self.stats["current_streak"] >= 3: + self.stats["pending_streak_notification"] = ( + f"🔥 Streak ended at {self.stats['current_streak']}!" + ) + + self.stats["current_streak"] = 0 + self.stats["last_round_won"] = False + + def get_stats(self): + """Return the current statistics.""" + stats = self.stats.copy() + if self.stats["pending_streak_notification"]: + self.stats["pending_streak_notification"] = None + return stats + + def start_new_round(self): + """Deal initial cards and reset the game state.""" + self.player_hand = [deal_card(), deal_card()] + self.player_total = calculate_hand_total(self.player_hand) + self.dealer_card = deal_card() + self.has_blackjack = has_blackjack(self.player_hand) \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100755 index 0000000..70f0457 --- /dev/null +++ b/constants.py @@ -0,0 +1,3 @@ +# Card-related constants +SUITS = ['clubs', 'diamonds', 'hearts', 'spades'] +RANKS = ['02', '03', '04', '05', '06', '07', '08', '09', '10', 'J', 'Q', 'K', 'A'] \ No newline at end of file diff --git a/game_logic.py b/game_logic.py new file mode 100755 index 0000000..381a6a0 --- /dev/null +++ b/game_logic.py @@ -0,0 +1,180 @@ +from constants import SUITS, RANKS +import random + +def deal_card(): + """Simulates drawing a card. Returns a card name (e.g., 'card_clubs_02', 'card_clubs_K').""" + return f"card_{random.choice(SUITS)}_{random.choice(RANKS)}" + +def calculate_hand_total(hand): + """Calculates the total value of a hand, adjusting for Aces if necessary.""" + numerical_values = [] + for card in hand: + # Extract the rank from the card name (e.g., 'card_clubs_02' -> '02') + rank = card.split('_')[-1] + + # Convert rank to numerical value + if rank in ['J', 'Q', 'K']: + numerical_values.append(10) + elif rank == 'A': + numerical_values.append(11) + else: + numerical_values.append(int(rank)) + + total = sum(numerical_values) + aces = numerical_values.count(11) + + # Adjust for Aces if the total exceeds 21 + while total > 21 and aces: + total -= 10 # Change an Ace from 11 to 1 + aces -= 1 + + return total + +def has_blackjack(hand): + """Check if the hand is a Blackjack (Ace + 10-value card).""" + return len(hand) == 2 and calculate_hand_total(hand) == 21 + +def can_split(player_hand): + """Checks if the player can split their hand.""" + if len(player_hand) != 2: + return False + + # Get ranks of both cards + rank1 = player_hand[0].split('_')[-1] + rank2 = player_hand[1].split('_')[-1] + + # Consider all 10-value cards as splittable with each other + ten_value_cards = ['10', 'J', 'Q', 'K'] + + # Case 1: Exact same rank + if rank1 == rank2: + return True + # Case 2: Both are 10-value cards + elif rank1 in ten_value_cards and rank2 in ten_value_cards: + return True + + return False + +def get_split_decision(player_hand, dealer_card): + """Returns the correct decision for splitting based on the player's pair and the dealer's card.""" + pair_card = player_hand[0].split('_')[-1] + + # Define splitting rules + if pair_card == "05": # 5s + return "Double", "Never split 5s; double down instead." + elif pair_card == "08": # 8s + return "Split", "Always split 8s." + elif pair_card in ['10', 'J', 'Q', 'K']: # 10s + return "Stand", "Never split 10s; stand instead." + elif pair_card == "A": # Aces + return "Split", "Always split Aces." + elif pair_card in ["02", "03", "04", "06", "07", "09"]: # Other pairs + if pair_card == "02" or pair_card == "03": + if dealer_card in ["02", "03", "04", "05", "06", "07"]: + return "Split", f"Split {pair_card}s vs dealer's 2–7. Note: This assumes doubling after splitting (DAS) is allowed. If DAS is not allowed, do not split {pair_card}s vs dealer's 2–3." + else: + return "Hit", f"Do not split {pair_card}s vs dealer's 8–Ace; hit instead." + elif pair_card == "04": # 4s + if dealer_card in ["05", "06"]: + return "Split", f"Split {pair_card}s vs dealer's 5–6. Note: This assumes doubling after splitting (DAS) is allowed. If DAS is not allowed, do not split {pair_card}s." + else: + return "Hit", f"Do not split {pair_card}s vs dealer's 2–4 or 7–Ace; hit instead." + elif pair_card == "06": # 6s + if dealer_card in ["02", "03", "04", "05", "06"]: + return "Split", f"Split {pair_card}s vs dealer's 2–6. Note: This assumes doubling after splitting (DAS) is allowed. If DAS is not allowed, do not split {pair_card}s vs dealer's 2." + else: + return "Hit", f"Do not split {pair_card}s vs dealer's 7–Ace; hit instead." + elif pair_card == "07": + if dealer_card in ["02", "03", "04", "05", "06", "07"]: + return "Split", f"Split {pair_card}s vs dealer's 2–7." + else: + return "Hit", f"Do not split {pair_card}s vs dealer's 8–Ace; hit instead." + elif pair_card == "09": + if dealer_card in ["02", "03", "04", "05", "06", "08", "09"]: + return "Split", f"Split {pair_card}s vs dealer's 2–6, 8, or 9." + else: + return "Stand", f"Do not split {pair_card}s vs dealer's 7, 10, or Ace; stand instead." + + return "No rule defined", "No rule defined for this pair." + +def get_correct_decision(player_hand, player_total, dealer_card): + """Returns the correct decision based on the player's hand and the dealer's card.""" + # Check if the hand is a soft total (contains an Ace counted as 11) + is_soft = 'A' in [card.split('_')[-1] for card in player_hand] and player_total <= 21 + + # Soft totals (Ace + 2 to Ace + 9) + if is_soft: + if player_total == 13: # Ace + 2 + if dealer_card.split('_')[-1] in ['05', '06']: + return "Double", "Double when you have A,2 vs dealer's 5–6." + else: + return "Hit", "Hit when you have A,2 vs dealer's 2–4 or 7–Ace." + elif player_total == 14: # Ace + 3 + if dealer_card.split('_')[-1] in ['05', '06']: + return "Double", "Double when you have A,3 vs dealer's 5–6." + else: + return "Hit", "Hit when you have A,3 vs dealer's 2–4 or 7–Ace." + elif player_total == 15: # Ace + 4 + if dealer_card.split('_')[-1] in ['04', '05', '06']: + return "Double", "Double when you have A,4 vs dealer's 4–6." + else: + return "Hit", "Hit when you have A,4 vs dealer's 2–3 or 7–Ace." + elif player_total == 16: # Ace + 5 + if dealer_card.split('_')[-1] in ['04', '05', '06']: + return "Double", "Double when you have A,5 vs dealer's 4–6." + else: + return "Hit", "Hit when you have A,5 vs dealer's 2–3 or 7–Ace." + elif player_total == 17: # Ace + 6 + if dealer_card.split('_')[-1] in ['03', '04', '05', '06']: + return "Double", "Double when you have A,6 vs dealer's 3–6." + else: + return "Hit", "Hit when you have A,6 vs dealer's 2 or 7–Ace." + elif player_total == 18: # Ace + 7 + if dealer_card.split('_')[-1] in ['03', '04', '05', '06']: # Double vs. 3–6 + return "Double", "Double when you have A,7 vs dealer's 3–6. Note: Some casinos may restrict doubling on certain hands, so check the rules." + elif dealer_card.split('_')[-1] in ['02', '07', '08']: # Stand vs. 2, 7, 8 + if dealer_card.split('_')[-1] == '02': + return "Stand", "Stand on A,7 vs dealer's 2. However, some players choose to Double in this situation as a risky option. Note: Some casinos may restrict doubling on certain hands, so check the rules." + else: + return "Stand", "Stand on A,7 vs dealer's 7 or 8." + else: # Hit vs. 9, 10, Ace + return "Hit", "Hit on A,7 vs dealer's 9, 10, or Ace, as the dealer has a strong chance of making a good hand." + elif player_total == 19: # Ace + 8 + if dealer_card.split('_')[-1] == '06': + return "Stand", "Stand on A,8 vs dealer's 6. However, some players choose to Double in this situation as a risky option. Note: This depends on casino rules." + else: + return "Stand", "Stand on A,8 vs dealer's 2–5 or 7–Ace." + elif player_total == 20: # Ace + 9 + return "Stand", "Stand on A,9 (soft 20)." + elif player_total == 21: # Ace + 10 + return "Stand", "Stand on A,10 (Blackjack)." + + # Hard totals (no Ace or Ace counted as 1) + if player_total < 9: + return "Hit", "Always Hit when your total is less than 9." + if player_total == 9: + if dealer_card.split('_')[-1] in ['03', '04', '05', '06']: + return "Double", "Double on 9 vs dealer's 3–6." + else: + return "Hit", "Hit on 9 vs dealer's 2, 7–Ace." + elif player_total == 10: + if dealer_card.split('_')[-1] in ['02', '03', '04', '05', '06', '07', '08', '09']: + return "Double", "Double on 10 vs dealer's 2–9." + else: + return "Hit", "Hit on 10 vs dealer's 10 or Ace." + elif player_total == 11: + return "Double", "Always Double on 11." + elif player_total == 12: + if dealer_card.split('_')[-1] in ['04', '05', '06']: + return "Stand", "Stand on 12 vs dealer's 4–6." + else: + return "Hit", "Hit on 12 vs dealer's 2–3 or 7–Ace." + elif 13 <= player_total <= 16: + if dealer_card.split('_')[-1] in ['02', '03', '04', '05', '06']: + return "Stand", "Stand on 13–16 vs dealer's 2–6." + else: + return "Hit", "Hit on 13–16 vs dealer's 7–Ace." + elif player_total >= 17: + return "Stand", "Always Stand on 17 or higher." + + return "No rule defined", "No rule defined for this scenario." \ No newline at end of file diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/components/__init__.py b/gui/components/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/components/buttons.py b/gui/components/buttons.py new file mode 100755 index 0000000..67f199c --- /dev/null +++ b/gui/components/buttons.py @@ -0,0 +1,103 @@ +import customtkinter as ctk + +class ActionButtons: + def __init__(self, parent, gui): + self.parent = parent + self.gui = gui # Reference to main GUI for callbacks + self._create_button_frame() + self._create_action_buttons() + self._create_control_buttons() + + def _create_button_frame(self): + """Matches original button frame creation""" + self.button_frame = ctk.CTkFrame(self.parent) + self.button_frame.pack(pady=10) + + def _create_action_buttons(self): + """Replicates original button creation exactly""" + self.hit_button = ctk.CTkButton( + self.button_frame, + text="Hit", + command=self.gui._hit, + fg_color="white", + text_color="black", + text_color_disabled="white", + font=("Arial", 20) + ) + self.hit_button.grid(row=0, column=0, padx=10, pady=10) + + self.stand_button = ctk.CTkButton( + self.button_frame, + text="Stand", + command=self.gui._stand, + fg_color="white", + text_color="black", + text_color_disabled="white", + font=("Arial", 20) + ) + self.stand_button.grid(row=0, column=1, padx=10, pady=10) + + self.double_button = ctk.CTkButton( + self.button_frame, + text="Double", + command=self.gui._double, + fg_color="white", + text_color="black", + text_color_disabled="white", + font=("Arial", 20) + ) + self.double_button.grid(row=1, column=0, padx=10, pady=10) + + self.split_button = ctk.CTkButton( + self.button_frame, + text="Split", + command=self.gui._split, + fg_color="white", + text_color="black", + text_color_disabled="white", + font=("Arial", 20) + ) + self.split_button.grid(row=1, column=1, padx=10, pady=10) + + def _create_control_buttons(self): + """Replicates hint and next round buttons""" + self.hint_button = ctk.CTkButton( + self.parent, + text="Hint", + command=self.gui.show_hint, + fg_color="white", + text_color="black", + font=("Arial", 20) + ) + self.hint_button.pack(pady=10) + + self.next_round_button = ctk.CTkButton( + self.parent, + text="Next Round", + command=self.gui.start_new_round, + fg_color="white", + text_color="black", + font=("Arial", 20) + ) + self.next_round_button.pack(pady=10) + self.next_round_button.configure(state="disabled") + + def _enable_action_buttons(self, enable): + """Original enable/disable functionality""" + state = "normal" if enable else "disabled" + self.hit_button.configure(state=state) + self.stand_button.configure(state=state) + self.double_button.configure(state=state) + self.split_button.configure(state=state) + + def _reset_button_colors(self): + """Original color reset""" + self.hit_button.configure(fg_color="white") + self.stand_button.configure(fg_color="white") + self.double_button.configure(fg_color="white") + self.split_button.configure(fg_color="white") + + def _mark_button(self, button, is_correct): + """Original button marking""" + color = "green" if is_correct else "red" + button.configure(fg_color=color) \ No newline at end of file diff --git a/gui/components/cards.py b/gui/components/cards.py new file mode 100755 index 0000000..69cc3b9 --- /dev/null +++ b/gui/components/cards.py @@ -0,0 +1,32 @@ +import customtkinter as ctk + +class CardDisplay: + def __init__(self, parent, card_images): + self.parent = parent + self.card_images = card_images + self.dealer_frame = None + self.player_frame = None + self._create_frames() + + def _create_frames(self): + """Create frames for dealer and player cards (matches original _create_frames)""" + self.dealer_frame = ctk.CTkFrame(self.parent) + self.dealer_frame.pack(pady=10) + self.player_frame = ctk.CTkFrame(self.parent) + self.player_frame.pack(pady=10) + + def update_display(self, dealer_card, player_hand): + """Matches original _update_display's card-related functionality""" + # Clear previous cards + for frame in [self.dealer_frame, self.player_frame]: + for widget in frame.winfo_children(): + widget.destroy() + + # Show dealer card + dealer_img = self.card_images.get(dealer_card, self.card_images["back"]) + ctk.CTkLabel(self.dealer_frame, image=dealer_img, text="").pack(side="left", padx=5) + + # Show player hand + for card in player_hand: + card_img = self.card_images.get(card, self.card_images["back"]) + ctk.CTkLabel(self.player_frame, image=card_img, text="").pack(side="left", padx=5) \ No newline at end of file diff --git a/gui/components/hints.py b/gui/components/hints.py new file mode 100755 index 0000000..430fa0f --- /dev/null +++ b/gui/components/hints.py @@ -0,0 +1,94 @@ +import customtkinter as ctk +from PIL import Image + +class HintSystem: + def __init__(self, parent): + self.parent = parent + self.hint_window = None + self.attached_windows = [] + self.hint_button = None # Will be set by main GUI + + def show_hint(self): + """Original hint display functionality""" + if not hasattr(self, "hint_window") or self.hint_window is None or not self.hint_window.winfo_exists(): + self._create_attached_window("hint", "Blackjack Strategy Hint", "images/hint.jpg", (550, 750)) + + def _create_attached_window(self, window_type, img_path, title, size): + """Original window creation logic""" + if hasattr(self, f"{window_type}_window"): + getattr(self, f"_close_{window_type}_window")() + + window = ctk.CTkToplevel(self.parent) + window.title(title) + window.geometry(f"{size[0]}x{size[1]}") + window.overrideredirect(True) + window.attributes("-topmost", True) + setattr(self, f"{window_type}_window", window) + + self.attached_windows.append(window) + + if window_type == "hint": + self._load_hint_image(window) + + self._update_attached_windows_position() + self.parent.bind("", lambda e: self._update_attached_windows_position()) + + def _update_attached_windows_position(self): + """Original window positioning logic""" + if not hasattr(self, 'parent') or not self.parent.winfo_exists(): + return + + hint_window_exists = (hasattr(self, "hint_window") and + self.hint_window is not None and + self.hint_window.winfo_exists()) + + if hint_window_exists: + main_x = self.parent.winfo_x() + main_y = self.parent.winfo_y() + main_width = self.parent.winfo_width() + hint_x = main_x + main_width + 10 + self.hint_window.geometry(f"+{hint_x}+{main_y}") + + def _close_hint_window(self): + """Safely close hint window with proper null checks""" + if hasattr(self, 'parent'): + self.parent.unbind("") + + # Close the window if it exists + if hasattr(self, "hint_window") and self.hint_window is not None: + if self.hint_window.winfo_exists(): + self.hint_window.destroy() + self.hint_window = None + + # Clean up references + self.attached_windows = [] + + # Rebind the Configure event if parent exists + if hasattr(self, 'parent') and self.parent.winfo_exists(): + self.parent.bind("", lambda e: self._update_attached_windows_position()) + + def _load_hint_image(self, window): + """Original image loading with error handling""" + try: + hint_image = Image.open("images/hint.jpg") + hint_image = hint_image.resize((500, 700)) + hint_ctk_image = ctk.CTkImage(hint_image, size=(500, 700)) + hint_label = ctk.CTkLabel(window, image=hint_ctk_image, text="") + hint_label.pack(padx=10, pady=10) + except FileNotFoundError: + error_label = ctk.CTkLabel(window, text="Hint image not found!", font=("Arial", 20)) + error_label.pack(padx=10, pady=10) + + def _disable_hint_button(self): + """Original button state control""" + if self.hint_button: + self.hint_button.configure(state="disabled") + + def _enable_hint_button(self): + """Original button state control""" + if self.hint_button: + self.hint_button.configure(state="normal") + + def set_hint_button(self, button): + """Allow main GUI to register its hint button""" + self.hint_button = button \ No newline at end of file diff --git a/gui/components/notifications.py b/gui/components/notifications.py new file mode 100755 index 0000000..af06129 --- /dev/null +++ b/gui/components/notifications.py @@ -0,0 +1,64 @@ +import customtkinter as ctk + +class NotificationSystem: + def __init__(self, parent): + self.parent = parent + self.streak_label = None + self.streak_notification = None + self.attached_windows = [] + self._create_streak_label() # Create label immediately + + def _update_streak_display(self, current_streak): + """Original streak display logic""" + if current_streak >= 3: + if not self.streak_label: + self._create_streak_label() + self.streak_label.configure(text=f"🔥 {current_streak}") + self.streak_label.lift() + elif self.streak_label: + self.streak_label.configure(text="") + + def _create_streak_label(self): + """Original streak label creation""" + self.streak_label = ctk.CTkLabel( + self.parent, + text="", + font=("Arial", 16, "bold"), + text_color="#FFD700", # Gold + bg_color="transparent" + ) + self.streak_label.place(relx=0.95, rely=0.02, anchor="ne") + + def _show_streak_notification(self, message): + """Original notification popup""" + notif = ctk.CTkToplevel(self.parent) + notif.overrideredirect(True) + + # Keep original styling + label = ctk.CTkLabel( + notif, + text=message, + font=("Arial", 14, "bold"), + fg_color="#3a7ebf", + text_color="white", + corner_radius=8, + padx=20, + pady=10 + ) + label.pack() + + # Original positioning logic + main_width = self.parent.winfo_width() + main_height = self.parent.winfo_height() + notif.update_idletasks() + notif_width = notif.winfo_width() + notif_height = notif.winfo_height() + + x_offset = main_width - notif_width - 20 + y_offset = 40 + + x_offset = max(10, min(x_offset, main_width - notif_width - 10)) + y_offset = max(10, min(y_offset, main_height - notif_height - 10)) + + notif.geometry(f"+{self.parent.winfo_x() + x_offset}+{self.parent.winfo_y() + y_offset}") + self.parent.after(1500, notif.destroy) \ No newline at end of file diff --git a/gui/components/stats.py b/gui/components/stats.py new file mode 100755 index 0000000..c0d4025 --- /dev/null +++ b/gui/components/stats.py @@ -0,0 +1,83 @@ +import customtkinter as ctk + +class StatsDisplay: + def __init__(self, parent): + self.parent = parent + self.stats_window = None + self.overlay = None + + def _load_stats_content(self, window, stats): + """Original stats content loading""" + for widget in window.winfo_children(): + widget.destroy() + + # Calculate accuracy (original logic) + total_decisions = stats["correct_decisions"] + stats["incorrect_decisions"] + accuracy = (stats["correct_decisions"] / total_decisions * 100) if total_decisions > 0 else 0 + + # Main stats display (original layout) + ctk.CTkLabel(window, text="Your Statistics", font=("Arial", 20, "bold")).pack(pady=10) + + stats_frame = ctk.CTkFrame(window) + stats_frame.pack(pady=5, padx=10, fill="x") + ctk.CTkLabel(stats_frame, text=f"Total Rounds: {stats['total_rounds']}", + font=("Arial", 16)).pack(anchor="w", pady=2) + ctk.CTkLabel(stats_frame, text=f"Accuracy: {accuracy:.1f}%", + font=("Arial", 16)).pack(anchor="w", pady=2) + ctk.CTkLabel(stats_frame, text=f"Current Streak: {stats['current_streak']}", + font=("Arial", 16)).pack(anchor="w", pady=2) + ctk.CTkLabel(stats_frame, text=f"Longest Streak: {stats['longest_streak']}", + font=("Arial", 16)).pack(anchor="w", pady=2) + + # Scenario breakdown (original implementation) + ctk.CTkLabel(window, text="Scenario Breakdown:", + font=("Arial", 16, "bold")).pack(pady=5) + scenario_frame = ctk.CTkFrame(window) + scenario_frame.pack(pady=5, padx=10, fill="x") + + for scenario, data in stats["by_scenario"].items(): + scenario_total = data["correct"] + data["incorrect"] + scenario_accuracy = (data["correct"] / scenario_total * 100) if scenario_total > 0 else 0 + ctk.CTkLabel( + scenario_frame, + text=f"{scenario.replace('_', ' ').title()}: {data['correct']}/{scenario_total} ({scenario_accuracy:.1f}%)", + font=("Arial", 14) + ).pack(anchor="w", pady=2) + + def _show_final_stats(self, stats): + """Original final stats window display""" + self.overlay = ctk.CTkFrame(self.parent, fg_color="black") + self.overlay.place(relwidth=1, relheight=1) + + self.stats_window = ctk.CTkToplevel(self.parent) + self.stats_window.title("Session Results") + self.stats_window.geometry("500x600") + self.stats_window.overrideredirect(True) + self.stats_window.grab_set() + + # Original positioning + stats_x = self.parent.winfo_x() + (self.parent.winfo_width() - 500) // 2 + stats_y = self.parent.winfo_y() + (self.parent.winfo_height() - 600) // 2 + self.stats_window.geometry(f"+{stats_x}+{stats_y}") + + self._load_stats_content(self.stats_window, stats) + + # Original close button + ctk.CTkButton( + self.stats_window, + text="Close", + command=self._terminate_app, + font=("Arial", 16), + height=40, + fg_color="#2aa44f" # Professional green + ).pack(pady=20, side="bottom", anchor="center") + + self.stats_window.protocol("WM_DELETE_WINDOW", self._terminate_app) + + def _terminate_app(self): + """Proper termination handling""" + if self.overlay and self.overlay.winfo_exists(): + self.overlay.destroy() + if hasattr(self, 'stats_window') and self.stats_window and self.stats_window.winfo_exists(): + self.stats_window.destroy() + self.parent.destroy() \ No newline at end of file diff --git a/gui/gui.py b/gui/gui.py new file mode 100755 index 0000000..579720a --- /dev/null +++ b/gui/gui.py @@ -0,0 +1,164 @@ +import customtkinter as ctk +from game_logic import can_split, get_split_decision, get_correct_decision +from .utils import load_card_images +from .components.cards import CardDisplay +from .components.buttons import ActionButtons +from .components.notifications import NotificationSystem +from .components.hints import HintSystem +from .components.stats import StatsDisplay + +class BlackjackGUI: + def __init__(self, blackjackGame): + self.game_logic = blackjackGame + self.card_images = load_card_images() + self._setup_main_window() + self._create_components() + self.start_new_round() + + def _setup_main_window(self): + """Initialize main application window""" + self.app = ctk.CTk() + self.app.title("Blackjack Theory Trainer") + self.app.geometry("600x650+800+200") + self.app.protocol("WM_DELETE_WINDOW", self._on_main_window_close) + ctk.set_appearance_mode("dark") + + def _create_components(self): + """Create all UI components""" + # Card display + self.card_display = CardDisplay(self.app, self.card_images) + + # Buttons (pass reference to self for callbacks) + self.buttons = ActionButtons(self.app, self) + + # Notifications + self.notifications = NotificationSystem(self.app) + + # Hint system + self.hints = HintSystem(self.app) + self.hints.set_hint_button(self.buttons.hint_button) + + # Stats display + self.stats = StatsDisplay(self.app) + + # Feedback label (only remaining widget in main file) + self.feedback_label = ctk.CTkLabel( + self.app, + text="", + font=("Arial", 20), + wraplength=550, + anchor="w" + ) + self.feedback_label.pack(pady=20, padx=10, fill="x") + + def start_new_round(self): + """Start a new round of the game""" + self._reset_ui() + self.game_logic.start_new_round() + self._update_display() + + def _reset_ui(self): + """Reset UI for new round""" + self.hints._close_hint_window() + self.buttons.hint_button.configure(state="normal") + self.feedback_label.configure(text="") + self.buttons.next_round_button.configure(state="disabled") + self.buttons._enable_action_buttons(True) + self.buttons._reset_button_colors() + + if hasattr(self.notifications, 'streak_label') and self.notifications.streak_label: + self.notifications.streak_label.configure(text="") + + def _update_display(self): + """Update all display elements""" + # Update card display + self.card_display.update_display( + self.game_logic.dealer_card, + self.game_logic.player_hand + ) + + # Handle blackjack + if self.game_logic.has_blackjack: + self.feedback_label.configure( + text="Blackjack! You win or get a push!", + text_color="green" + ) + self.buttons._enable_action_buttons(False) + self.buttons.next_round_button.configure(state="normal") + + # Handle split possibility + can_split_bool = can_split(self.game_logic.player_hand) + if can_split_bool: + self.buttons.split_button.configure(state="normal") + self.feedback_label.configure( + text="You have a pair! Consider splitting.", + text_color="white" + ) + else: + self.buttons.split_button.configure(state="disabled") + + # Update notifications + stats = self.game_logic.get_stats() + self.notifications._update_streak_display(stats["current_streak"]) + if stats.get("pending_streak_notification"): + self.notifications._show_streak_notification(stats["pending_streak_notification"]) + + def _hit(self): + self._handle_action("Hit", self.buttons.hit_button) + + def _stand(self): + self._handle_action("Stand", self.buttons.stand_button) + + def _double(self): + self._handle_action("Double", self.buttons.double_button) + + def _split(self): + self._handle_action("Split", self.buttons.split_button) + + def _handle_action(self, action, button): + """Process player actions""" + # Determine scenario type + scenario_type = "hard_totals" + if can_split(self.game_logic.player_hand): + scenario_type = "pairs" + elif 'A' in [card.split('_')[-1] for card in self.game_logic.player_hand]: + scenario_type = "soft_totals" + + # Check if player can split + if can_split(self.game_logic.player_hand): + decision, feedback = get_split_decision( + self.game_logic.player_hand, + self.game_logic.dealer_card + ) + correct = action == decision + else: + decision, feedback = get_correct_decision( + self.game_logic.player_hand, + self.game_logic.player_total, + self.game_logic.dealer_card + ) + correct = action == decision + + # Update UI + color = "green" if correct else "red" + self.feedback_label.configure(text=feedback, text_color=color) + self.buttons._mark_button(button, correct) + self.game_logic.update_stats(correct, scenario_type) + + # Update button states + self.buttons._enable_action_buttons(False) + self.buttons.next_round_button.configure(state="normal") + + def _on_main_window_close(self): + """Handle window close event""" + self.app.configure(state="disabled") + self.stats._show_final_stats(self.game_logic.get_stats()) + + def show_hint(self): + """Delegate hint display to the HintSystem""" + self.hints._disable_hint_button() + self.hints.show_hint() + + def run(self): + """Start application main loop""" + self.app.mainloop() \ No newline at end of file diff --git a/gui/utils.py b/gui/utils.py new file mode 100755 index 0000000..6f409d9 --- /dev/null +++ b/gui/utils.py @@ -0,0 +1,24 @@ +from PIL import Image +from constants import SUITS, RANKS +import customtkinter as ctk + +def load_card_images(): + """Load card images into memory.""" + card_images = {} + for suit in SUITS: + for rank in RANKS: + card_name = f"card_{suit}_{rank}" + try: + image = Image.open(f"images/cards/{card_name}.png") + card_images[card_name] = ctk.CTkImage(image, size=(125, 125)) + except FileNotFoundError: + print(f"Image not found: {card_name}.png") + + # Load the back image for the dealer's hidden card + try: + back_image = Image.open("images/cards/card_back.png") + card_images["back"] = ctk.CTkImage(back_image, size=(125, 125)) + except FileNotFoundError: + print("Back image not found: back.png") + + return card_images \ No newline at end of file diff --git a/images/cards/card_back.png b/images/cards/card_back.png new file mode 100755 index 0000000..2262a80 Binary files /dev/null and b/images/cards/card_back.png differ diff --git a/images/cards/card_clubs_02.png b/images/cards/card_clubs_02.png new file mode 100755 index 0000000..6dcbe9b Binary files /dev/null and b/images/cards/card_clubs_02.png differ diff --git a/images/cards/card_clubs_03.png b/images/cards/card_clubs_03.png new file mode 100755 index 0000000..7379710 Binary files /dev/null and b/images/cards/card_clubs_03.png differ diff --git a/images/cards/card_clubs_04.png b/images/cards/card_clubs_04.png new file mode 100755 index 0000000..8e34ac4 Binary files /dev/null and b/images/cards/card_clubs_04.png differ diff --git a/images/cards/card_clubs_05.png b/images/cards/card_clubs_05.png new file mode 100755 index 0000000..93af4e3 Binary files /dev/null and b/images/cards/card_clubs_05.png differ diff --git a/images/cards/card_clubs_06.png b/images/cards/card_clubs_06.png new file mode 100755 index 0000000..c3aaaf2 Binary files /dev/null and b/images/cards/card_clubs_06.png differ diff --git a/images/cards/card_clubs_07.png b/images/cards/card_clubs_07.png new file mode 100755 index 0000000..823f01c Binary files /dev/null and b/images/cards/card_clubs_07.png differ diff --git a/images/cards/card_clubs_08.png b/images/cards/card_clubs_08.png new file mode 100755 index 0000000..b7fd62a Binary files /dev/null and b/images/cards/card_clubs_08.png differ diff --git a/images/cards/card_clubs_09.png b/images/cards/card_clubs_09.png new file mode 100755 index 0000000..6966b26 Binary files /dev/null and b/images/cards/card_clubs_09.png differ diff --git a/images/cards/card_clubs_10.png b/images/cards/card_clubs_10.png new file mode 100755 index 0000000..ec464cc Binary files /dev/null and b/images/cards/card_clubs_10.png differ diff --git a/images/cards/card_clubs_A.png b/images/cards/card_clubs_A.png new file mode 100755 index 0000000..9f7ef43 Binary files /dev/null and b/images/cards/card_clubs_A.png differ diff --git a/images/cards/card_clubs_J.png b/images/cards/card_clubs_J.png new file mode 100755 index 0000000..5c9a6f1 Binary files /dev/null and b/images/cards/card_clubs_J.png differ diff --git a/images/cards/card_clubs_K.png b/images/cards/card_clubs_K.png new file mode 100755 index 0000000..480b953 Binary files /dev/null and b/images/cards/card_clubs_K.png differ diff --git a/images/cards/card_clubs_Q.png b/images/cards/card_clubs_Q.png new file mode 100755 index 0000000..b1514d1 Binary files /dev/null and b/images/cards/card_clubs_Q.png differ diff --git a/images/cards/card_diamonds_02.png b/images/cards/card_diamonds_02.png new file mode 100755 index 0000000..6174b9b Binary files /dev/null and b/images/cards/card_diamonds_02.png differ diff --git a/images/cards/card_diamonds_03.png b/images/cards/card_diamonds_03.png new file mode 100755 index 0000000..eba92c6 Binary files /dev/null and b/images/cards/card_diamonds_03.png differ diff --git a/images/cards/card_diamonds_04.png b/images/cards/card_diamonds_04.png new file mode 100755 index 0000000..a096f77 Binary files /dev/null and b/images/cards/card_diamonds_04.png differ diff --git a/images/cards/card_diamonds_05.png b/images/cards/card_diamonds_05.png new file mode 100755 index 0000000..08e54b0 Binary files /dev/null and b/images/cards/card_diamonds_05.png differ diff --git a/images/cards/card_diamonds_06.png b/images/cards/card_diamonds_06.png new file mode 100755 index 0000000..c4a86e8 Binary files /dev/null and b/images/cards/card_diamonds_06.png differ diff --git a/images/cards/card_diamonds_07.png b/images/cards/card_diamonds_07.png new file mode 100755 index 0000000..53c4d8a Binary files /dev/null and b/images/cards/card_diamonds_07.png differ diff --git a/images/cards/card_diamonds_08.png b/images/cards/card_diamonds_08.png new file mode 100755 index 0000000..2331cfb Binary files /dev/null and b/images/cards/card_diamonds_08.png differ diff --git a/images/cards/card_diamonds_09.png b/images/cards/card_diamonds_09.png new file mode 100755 index 0000000..5ebd23c Binary files /dev/null and b/images/cards/card_diamonds_09.png differ diff --git a/images/cards/card_diamonds_10.png b/images/cards/card_diamonds_10.png new file mode 100755 index 0000000..f2bd803 Binary files /dev/null and b/images/cards/card_diamonds_10.png differ diff --git a/images/cards/card_diamonds_A.png b/images/cards/card_diamonds_A.png new file mode 100755 index 0000000..b3f0dbd Binary files /dev/null and b/images/cards/card_diamonds_A.png differ diff --git a/images/cards/card_diamonds_J.png b/images/cards/card_diamonds_J.png new file mode 100755 index 0000000..fcf9b73 Binary files /dev/null and b/images/cards/card_diamonds_J.png differ diff --git a/images/cards/card_diamonds_K.png b/images/cards/card_diamonds_K.png new file mode 100755 index 0000000..6f444ce Binary files /dev/null and b/images/cards/card_diamonds_K.png differ diff --git a/images/cards/card_diamonds_Q.png b/images/cards/card_diamonds_Q.png new file mode 100755 index 0000000..4bb96d6 Binary files /dev/null and b/images/cards/card_diamonds_Q.png differ diff --git a/images/cards/card_hearts_02.png b/images/cards/card_hearts_02.png new file mode 100755 index 0000000..1d23059 Binary files /dev/null and b/images/cards/card_hearts_02.png differ diff --git a/images/cards/card_hearts_03.png b/images/cards/card_hearts_03.png new file mode 100755 index 0000000..732c8ca Binary files /dev/null and b/images/cards/card_hearts_03.png differ diff --git a/images/cards/card_hearts_04.png b/images/cards/card_hearts_04.png new file mode 100755 index 0000000..077a94a Binary files /dev/null and b/images/cards/card_hearts_04.png differ diff --git a/images/cards/card_hearts_05.png b/images/cards/card_hearts_05.png new file mode 100755 index 0000000..831f13b Binary files /dev/null and b/images/cards/card_hearts_05.png differ diff --git a/images/cards/card_hearts_06.png b/images/cards/card_hearts_06.png new file mode 100755 index 0000000..09b77a7 Binary files /dev/null and b/images/cards/card_hearts_06.png differ diff --git a/images/cards/card_hearts_07.png b/images/cards/card_hearts_07.png new file mode 100755 index 0000000..2bb80e6 Binary files /dev/null and b/images/cards/card_hearts_07.png differ diff --git a/images/cards/card_hearts_08.png b/images/cards/card_hearts_08.png new file mode 100755 index 0000000..018dab3 Binary files /dev/null and b/images/cards/card_hearts_08.png differ diff --git a/images/cards/card_hearts_09.png b/images/cards/card_hearts_09.png new file mode 100755 index 0000000..cf86985 Binary files /dev/null and b/images/cards/card_hearts_09.png differ diff --git a/images/cards/card_hearts_10.png b/images/cards/card_hearts_10.png new file mode 100755 index 0000000..524385a Binary files /dev/null and b/images/cards/card_hearts_10.png differ diff --git a/images/cards/card_hearts_A.png b/images/cards/card_hearts_A.png new file mode 100755 index 0000000..058c125 Binary files /dev/null and b/images/cards/card_hearts_A.png differ diff --git a/images/cards/card_hearts_J.png b/images/cards/card_hearts_J.png new file mode 100755 index 0000000..fecd639 Binary files /dev/null and b/images/cards/card_hearts_J.png differ diff --git a/images/cards/card_hearts_K.png b/images/cards/card_hearts_K.png new file mode 100755 index 0000000..170e79b Binary files /dev/null and b/images/cards/card_hearts_K.png differ diff --git a/images/cards/card_hearts_Q.png b/images/cards/card_hearts_Q.png new file mode 100755 index 0000000..55a242e Binary files /dev/null and b/images/cards/card_hearts_Q.png differ diff --git a/images/cards/card_spades_02.png b/images/cards/card_spades_02.png new file mode 100755 index 0000000..087fa70 Binary files /dev/null and b/images/cards/card_spades_02.png differ diff --git a/images/cards/card_spades_03.png b/images/cards/card_spades_03.png new file mode 100755 index 0000000..512debc Binary files /dev/null and b/images/cards/card_spades_03.png differ diff --git a/images/cards/card_spades_04.png b/images/cards/card_spades_04.png new file mode 100755 index 0000000..a107abd Binary files /dev/null and b/images/cards/card_spades_04.png differ diff --git a/images/cards/card_spades_05.png b/images/cards/card_spades_05.png new file mode 100755 index 0000000..16c17c0 Binary files /dev/null and b/images/cards/card_spades_05.png differ diff --git a/images/cards/card_spades_06.png b/images/cards/card_spades_06.png new file mode 100755 index 0000000..aa64b42 Binary files /dev/null and b/images/cards/card_spades_06.png differ diff --git a/images/cards/card_spades_07.png b/images/cards/card_spades_07.png new file mode 100755 index 0000000..1e91534 Binary files /dev/null and b/images/cards/card_spades_07.png differ diff --git a/images/cards/card_spades_08.png b/images/cards/card_spades_08.png new file mode 100755 index 0000000..9272087 Binary files /dev/null and b/images/cards/card_spades_08.png differ diff --git a/images/cards/card_spades_09.png b/images/cards/card_spades_09.png new file mode 100755 index 0000000..6de308a Binary files /dev/null and b/images/cards/card_spades_09.png differ diff --git a/images/cards/card_spades_10.png b/images/cards/card_spades_10.png new file mode 100755 index 0000000..4dd3dfe Binary files /dev/null and b/images/cards/card_spades_10.png differ diff --git a/images/cards/card_spades_A.png b/images/cards/card_spades_A.png new file mode 100755 index 0000000..22f7b13 Binary files /dev/null and b/images/cards/card_spades_A.png differ diff --git a/images/cards/card_spades_J.png b/images/cards/card_spades_J.png new file mode 100755 index 0000000..6b5231b Binary files /dev/null and b/images/cards/card_spades_J.png differ diff --git a/images/cards/card_spades_K.png b/images/cards/card_spades_K.png new file mode 100755 index 0000000..e9fe37f Binary files /dev/null and b/images/cards/card_spades_K.png differ diff --git a/images/cards/card_spades_Q.png b/images/cards/card_spades_Q.png new file mode 100755 index 0000000..9611b5e Binary files /dev/null and b/images/cards/card_spades_Q.png differ diff --git a/images/hint.jpg b/images/hint.jpg new file mode 100755 index 0000000..e85a686 Binary files /dev/null and b/images/hint.jpg differ diff --git a/images/hint.jpgZone.Identifier b/images/hint.jpgZone.Identifier new file mode 100755 index 0000000..a309ddd --- /dev/null +++ b/images/hint.jpgZone.Identifier @@ -0,0 +1,4 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://www.blackjackapprenticeship.com/wp-content/uploads/2018/08/BJA_Basic_Strategy.jpg +HostUrl=https://www.blackjackapprenticeship.com/wp-content/uploads/2018/08/BJA_Basic_Strategy.jpg diff --git a/main.py b/main.py new file mode 100755 index 0000000..14fdbc9 --- /dev/null +++ b/main.py @@ -0,0 +1,7 @@ +from blackjack_game import BlackjackGame +from gui.gui import BlackjackGUI + +if __name__ == "__main__": + game = BlackjackGame() + gui = BlackjackGUI(game) + gui.run() \ No newline at end of file