init commit with project version 1.0

This commit is contained in:
Maik 2026-06-09 20:07:08 +02:00
parent a4337222ae
commit 771b3c8cf0
68 changed files with 824 additions and 0 deletions

66
blackjack_game.py Executable file
View 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
View 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
View 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 27. Note: This assumes doubling after splitting (DAS) is allowed. If DAS is not allowed, do not split {pair_card}s vs dealer's 23."
else:
return "Hit", f"Do not split {pair_card}s vs dealer's 8Ace; hit instead."
elif pair_card == "04": # 4s
if dealer_card in ["05", "06"]:
return "Split", f"Split {pair_card}s vs dealer's 56. 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 24 or 7Ace; 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 26. 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 7Ace; 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 27."
else:
return "Hit", f"Do not split {pair_card}s vs dealer's 8Ace; 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 26, 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 56."
else:
return "Hit", "Hit when you have A,2 vs dealer's 24 or 7Ace."
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 56."
else:
return "Hit", "Hit when you have A,3 vs dealer's 24 or 7Ace."
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 46."
else:
return "Hit", "Hit when you have A,4 vs dealer's 23 or 7Ace."
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 46."
else:
return "Hit", "Hit when you have A,5 vs dealer's 23 or 7Ace."
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 36."
else:
return "Hit", "Hit when you have A,6 vs dealer's 2 or 7Ace."
elif player_total == 18: # Ace + 7
if dealer_card.split('_')[-1] in ['03', '04', '05', '06']: # Double vs. 36
return "Double", "Double when you have A,7 vs dealer's 36. 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 25 or 7Ace."
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 36."
else:
return "Hit", "Hit on 9 vs dealer's 2, 7Ace."
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 29."
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 46."
else:
return "Hit", "Hit on 12 vs dealer's 23 or 7Ace."
elif 13 <= player_total <= 16:
if dealer_card.split('_')[-1] in ['02', '03', '04', '05', '06']:
return "Stand", "Stand on 1316 vs dealer's 26."
else:
return "Hit", "Hit on 1316 vs dealer's 7Ace."
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
View File

0
gui/components/__init__.py Executable file
View File

103
gui/components/buttons.py Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

BIN
images/cards/card_clubs_02.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

BIN
images/cards/card_clubs_03.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

BIN
images/cards/card_clubs_04.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

BIN
images/cards/card_clubs_05.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

BIN
images/cards/card_clubs_06.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

BIN
images/cards/card_clubs_07.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

BIN
images/cards/card_clubs_08.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

BIN
images/cards/card_clubs_09.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

BIN
images/cards/card_clubs_10.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

BIN
images/cards/card_clubs_A.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

BIN
images/cards/card_clubs_J.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

BIN
images/cards/card_clubs_K.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

BIN
images/cards/card_clubs_Q.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

BIN
images/cards/card_diamonds_02.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

BIN
images/cards/card_diamonds_03.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

BIN
images/cards/card_diamonds_04.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

BIN
images/cards/card_diamonds_05.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

BIN
images/cards/card_diamonds_06.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

BIN
images/cards/card_diamonds_07.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

BIN
images/cards/card_diamonds_08.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

BIN
images/cards/card_diamonds_09.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

BIN
images/cards/card_diamonds_10.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

BIN
images/cards/card_diamonds_A.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

BIN
images/cards/card_diamonds_J.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

BIN
images/cards/card_diamonds_K.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

BIN
images/cards/card_diamonds_Q.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

BIN
images/cards/card_hearts_02.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

BIN
images/cards/card_hearts_03.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

BIN
images/cards/card_hearts_04.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

BIN
images/cards/card_hearts_05.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

BIN
images/cards/card_hearts_06.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

BIN
images/cards/card_hearts_07.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

BIN
images/cards/card_hearts_08.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

BIN
images/cards/card_hearts_09.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

BIN
images/cards/card_hearts_10.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

BIN
images/cards/card_hearts_A.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

BIN
images/cards/card_hearts_J.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

BIN
images/cards/card_hearts_K.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

BIN
images/cards/card_hearts_Q.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

BIN
images/cards/card_spades_02.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

BIN
images/cards/card_spades_03.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

BIN
images/cards/card_spades_04.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

BIN
images/cards/card_spades_05.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

BIN
images/cards/card_spades_06.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

BIN
images/cards/card_spades_07.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

BIN
images/cards/card_spades_08.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

BIN
images/cards/card_spades_09.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

BIN
images/cards/card_spades_10.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

BIN
images/cards/card_spades_A.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

BIN
images/cards/card_spades_J.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

BIN
images/cards/card_spades_K.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

BIN
images/cards/card_spades_Q.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

BIN
images/hint.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

View 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

7
main.py Executable file
View File

@ -0,0 +1,7 @@
from blackjack_game import BlackjackGame
from gui.gui import BlackjackGUI
if __name__ == "__main__":
game = BlackjackGame()
gui = BlackjackGUI(game)
gui.run()