init commit with project version 1.0
66
blackjack_game.py
Executable file
@ -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)
|
||||
3
constants.py
Executable file
@ -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']
|
||||
180
game_logic.py
Executable file
@ -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."
|
||||
0
gui/__init__.py
Executable file
0
gui/components/__init__.py
Executable file
103
gui/components/buttons.py
Executable file
@ -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)
|
||||
32
gui/components/cards.py
Executable file
@ -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)
|
||||
94
gui/components/hints.py
Executable file
@ -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("<Configure>", 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("<Configure>")
|
||||
|
||||
# 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("<Configure>", 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
|
||||
64
gui/components/notifications.py
Executable file
@ -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)
|
||||
83
gui/components/stats.py
Executable file
@ -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()
|
||||
164
gui/gui.py
Executable file
@ -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()
|
||||
24
gui/utils.py
Executable file
@ -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
|
||||
BIN
images/cards/card_back.png
Executable file
|
After Width: | Height: | Size: 231 B |
BIN
images/cards/card_clubs_02.png
Executable file
|
After Width: | Height: | Size: 241 B |
BIN
images/cards/card_clubs_03.png
Executable file
|
After Width: | Height: | Size: 244 B |
BIN
images/cards/card_clubs_04.png
Executable file
|
After Width: | Height: | Size: 228 B |
BIN
images/cards/card_clubs_05.png
Executable file
|
After Width: | Height: | Size: 253 B |
BIN
images/cards/card_clubs_06.png
Executable file
|
After Width: | Height: | Size: 250 B |
BIN
images/cards/card_clubs_07.png
Executable file
|
After Width: | Height: | Size: 253 B |
BIN
images/cards/card_clubs_08.png
Executable file
|
After Width: | Height: | Size: 262 B |
BIN
images/cards/card_clubs_09.png
Executable file
|
After Width: | Height: | Size: 271 B |
BIN
images/cards/card_clubs_10.png
Executable file
|
After Width: | Height: | Size: 277 B |
BIN
images/cards/card_clubs_A.png
Executable file
|
After Width: | Height: | Size: 218 B |
BIN
images/cards/card_clubs_J.png
Executable file
|
After Width: | Height: | Size: 324 B |
BIN
images/cards/card_clubs_K.png
Executable file
|
After Width: | Height: | Size: 335 B |
BIN
images/cards/card_clubs_Q.png
Executable file
|
After Width: | Height: | Size: 322 B |
BIN
images/cards/card_diamonds_02.png
Executable file
|
After Width: | Height: | Size: 254 B |
BIN
images/cards/card_diamonds_03.png
Executable file
|
After Width: | Height: | Size: 254 B |
BIN
images/cards/card_diamonds_04.png
Executable file
|
After Width: | Height: | Size: 234 B |
BIN
images/cards/card_diamonds_05.png
Executable file
|
After Width: | Height: | Size: 268 B |
BIN
images/cards/card_diamonds_06.png
Executable file
|
After Width: | Height: | Size: 255 B |
BIN
images/cards/card_diamonds_07.png
Executable file
|
After Width: | Height: | Size: 266 B |
BIN
images/cards/card_diamonds_08.png
Executable file
|
After Width: | Height: | Size: 269 B |
BIN
images/cards/card_diamonds_09.png
Executable file
|
After Width: | Height: | Size: 273 B |
BIN
images/cards/card_diamonds_10.png
Executable file
|
After Width: | Height: | Size: 274 B |
BIN
images/cards/card_diamonds_A.png
Executable file
|
After Width: | Height: | Size: 235 B |
BIN
images/cards/card_diamonds_J.png
Executable file
|
After Width: | Height: | Size: 335 B |
BIN
images/cards/card_diamonds_K.png
Executable file
|
After Width: | Height: | Size: 341 B |
BIN
images/cards/card_diamonds_Q.png
Executable file
|
After Width: | Height: | Size: 331 B |
BIN
images/cards/card_hearts_02.png
Executable file
|
After Width: | Height: | Size: 267 B |
BIN
images/cards/card_hearts_03.png
Executable file
|
After Width: | Height: | Size: 269 B |
BIN
images/cards/card_hearts_04.png
Executable file
|
After Width: | Height: | Size: 253 B |
BIN
images/cards/card_hearts_05.png
Executable file
|
After Width: | Height: | Size: 289 B |
BIN
images/cards/card_hearts_06.png
Executable file
|
After Width: | Height: | Size: 272 B |
BIN
images/cards/card_hearts_07.png
Executable file
|
After Width: | Height: | Size: 286 B |
BIN
images/cards/card_hearts_08.png
Executable file
|
After Width: | Height: | Size: 301 B |
BIN
images/cards/card_hearts_09.png
Executable file
|
After Width: | Height: | Size: 295 B |
BIN
images/cards/card_hearts_10.png
Executable file
|
After Width: | Height: | Size: 315 B |
BIN
images/cards/card_hearts_A.png
Executable file
|
After Width: | Height: | Size: 240 B |
BIN
images/cards/card_hearts_J.png
Executable file
|
After Width: | Height: | Size: 352 B |
BIN
images/cards/card_hearts_K.png
Executable file
|
After Width: | Height: | Size: 353 B |
BIN
images/cards/card_hearts_Q.png
Executable file
|
After Width: | Height: | Size: 343 B |
BIN
images/cards/card_spades_02.png
Executable file
|
After Width: | Height: | Size: 243 B |
BIN
images/cards/card_spades_03.png
Executable file
|
After Width: | Height: | Size: 245 B |
BIN
images/cards/card_spades_04.png
Executable file
|
After Width: | Height: | Size: 235 B |
BIN
images/cards/card_spades_05.png
Executable file
|
After Width: | Height: | Size: 260 B |
BIN
images/cards/card_spades_06.png
Executable file
|
After Width: | Height: | Size: 251 B |
BIN
images/cards/card_spades_07.png
Executable file
|
After Width: | Height: | Size: 254 B |
BIN
images/cards/card_spades_08.png
Executable file
|
After Width: | Height: | Size: 267 B |
BIN
images/cards/card_spades_09.png
Executable file
|
After Width: | Height: | Size: 274 B |
BIN
images/cards/card_spades_10.png
Executable file
|
After Width: | Height: | Size: 270 B |
BIN
images/cards/card_spades_A.png
Executable file
|
After Width: | Height: | Size: 219 B |
BIN
images/cards/card_spades_J.png
Executable file
|
After Width: | Height: | Size: 326 B |
BIN
images/cards/card_spades_K.png
Executable file
|
After Width: | Height: | Size: 341 B |
BIN
images/cards/card_spades_Q.png
Executable file
|
After Width: | Height: | Size: 326 B |
BIN
images/hint.jpg
Executable file
|
After Width: | Height: | Size: 692 KiB |
4
images/hint.jpgZone.Identifier
Executable file
@ -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
|
||||