Compare commits
7 Commits
4b1f5d9874
...
a0d4ec6fc9
| Author | SHA1 | Date | |
|---|---|---|---|
| a0d4ec6fc9 | |||
|
|
97ffb52703 | ||
|
|
9d8752ab40 | ||
|
|
16103310c0 | ||
|
|
f49dfe10e5 | ||
|
|
4299dd48c5 | ||
|
|
a1b5805727 |
@ -6,7 +6,6 @@ class ActionButtons:
|
|||||||
self.gui = gui # Reference to main GUI for callbacks
|
self.gui = gui # Reference to main GUI for callbacks
|
||||||
self._create_button_frame()
|
self._create_button_frame()
|
||||||
self._create_action_buttons()
|
self._create_action_buttons()
|
||||||
self._create_control_buttons()
|
|
||||||
|
|
||||||
def _create_button_frame(self):
|
def _create_button_frame(self):
|
||||||
"""Matches original button frame creation"""
|
"""Matches original button frame creation"""
|
||||||
@ -15,73 +14,16 @@ class ActionButtons:
|
|||||||
|
|
||||||
def _create_action_buttons(self):
|
def _create_action_buttons(self):
|
||||||
"""Replicates original button creation exactly"""
|
"""Replicates original button creation exactly"""
|
||||||
self.hit_button = ctk.CTkButton(
|
common = dict(fg_color="white", hover_color="white", text_color="black", text_color_disabled="white", font=("Arial", 20))
|
||||||
self.button_frame,
|
self.hit_button = ctk.CTkButton(self.button_frame, text="Hit", command=self.gui._hit, **common)
|
||||||
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.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, **common)
|
||||||
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.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, **common)
|
||||||
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.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, **common)
|
||||||
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)
|
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):
|
def _enable_action_buttons(self, enable):
|
||||||
"""Original enable/disable functionality"""
|
"""Original enable/disable functionality"""
|
||||||
state = "normal" if enable else "disabled"
|
state = "normal" if enable else "disabled"
|
||||||
@ -92,12 +34,10 @@ class ActionButtons:
|
|||||||
|
|
||||||
def _reset_button_colors(self):
|
def _reset_button_colors(self):
|
||||||
"""Original color reset"""
|
"""Original color reset"""
|
||||||
self.hit_button.configure(fg_color="white")
|
for b in (self.hit_button, self.stand_button, self.double_button, self.split_button):
|
||||||
self.stand_button.configure(fg_color="white")
|
b.configure(fg_color="white", hover_color="white")
|
||||||
self.double_button.configure(fg_color="white")
|
|
||||||
self.split_button.configure(fg_color="white")
|
|
||||||
|
|
||||||
def _mark_button(self, button, is_correct):
|
def _mark_button(self, button, is_correct):
|
||||||
"""Original button marking"""
|
"""Original button marking"""
|
||||||
color = "green" if is_correct else "red"
|
color = "green" if is_correct else "red"
|
||||||
button.configure(fg_color=color)
|
button.configure(fg_color=color, hover_color=color)
|
||||||
@ -53,6 +53,7 @@ class StatsDisplay:
|
|||||||
self.stats_window.title("Session Results")
|
self.stats_window.title("Session Results")
|
||||||
self.stats_window.geometry("500x600")
|
self.stats_window.geometry("500x600")
|
||||||
self.stats_window.overrideredirect(True)
|
self.stats_window.overrideredirect(True)
|
||||||
|
self.stats_window.update_idletasks()
|
||||||
self.stats_window.grab_set()
|
self.stats_window.grab_set()
|
||||||
|
|
||||||
# Original positioning
|
# Original positioning
|
||||||
@ -69,11 +70,48 @@ class StatsDisplay:
|
|||||||
command=self._terminate_app,
|
command=self._terminate_app,
|
||||||
font=("Arial", 16),
|
font=("Arial", 16),
|
||||||
height=40,
|
height=40,
|
||||||
fg_color="#2aa44f" # Professional green
|
fg_color="#2aa44f",
|
||||||
|
hover_color="#238c44"
|
||||||
).pack(pady=20, side="bottom", anchor="center")
|
).pack(pady=20, side="bottom", anchor="center")
|
||||||
|
|
||||||
self.stats_window.protocol("WM_DELETE_WINDOW", self._terminate_app)
|
self.stats_window.protocol("WM_DELETE_WINDOW", self._terminate_app)
|
||||||
|
|
||||||
|
def _show_exit_popup(self, stats, on_back_to_start):
|
||||||
|
"""Exit stats popup with back-to-start button"""
|
||||||
|
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.update_idletasks()
|
||||||
|
self.stats_window.grab_set()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
ctk.CTkButton(
|
||||||
|
self.stats_window,
|
||||||
|
text="Back to Start",
|
||||||
|
command=lambda: self._close_exit_popup(on_back_to_start),
|
||||||
|
font=("Arial", 16),
|
||||||
|
height=40,
|
||||||
|
fg_color="#2aa44f",
|
||||||
|
hover_color="#238c44"
|
||||||
|
).pack(pady=20, side="bottom", anchor="center")
|
||||||
|
|
||||||
|
def _close_exit_popup(self, callback):
|
||||||
|
"""Close the exit popup and trigger callback"""
|
||||||
|
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()
|
||||||
|
callback()
|
||||||
|
|
||||||
def _terminate_app(self):
|
def _terminate_app(self):
|
||||||
"""Proper termination handling"""
|
"""Proper termination handling"""
|
||||||
if self.overlay and self.overlay.winfo_exists():
|
if self.overlay and self.overlay.winfo_exists():
|
||||||
|
|||||||
151
gui/gui.py
151
gui/gui.py
@ -1,4 +1,5 @@
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
from blackjack_game import BlackjackGame
|
||||||
from game_logic import can_split, get_split_decision, get_correct_decision
|
from game_logic import can_split, get_split_decision, get_correct_decision
|
||||||
from .utils import load_card_images
|
from .utils import load_card_images
|
||||||
from .components.cards import CardDisplay
|
from .components.cards import CardDisplay
|
||||||
@ -13,7 +14,7 @@ class BlackjackGUI:
|
|||||||
self.card_images = load_card_images()
|
self.card_images = load_card_images()
|
||||||
self._setup_main_window()
|
self._setup_main_window()
|
||||||
self._create_components()
|
self._create_components()
|
||||||
self.start_new_round()
|
self._create_start_screen()
|
||||||
|
|
||||||
def _setup_main_window(self):
|
def _setup_main_window(self):
|
||||||
"""Initialize main application window"""
|
"""Initialize main application window"""
|
||||||
@ -28,29 +29,66 @@ class BlackjackGUI:
|
|||||||
# Card display
|
# Card display
|
||||||
self.card_display = CardDisplay(self.app, self.card_images)
|
self.card_display = CardDisplay(self.app, self.card_images)
|
||||||
|
|
||||||
# Buttons (pass reference to self for callbacks)
|
# Action buttons (Hit, Stand, Double, Split)
|
||||||
self.buttons = ActionButtons(self.app, self)
|
self.buttons = ActionButtons(self.app, self)
|
||||||
|
|
||||||
|
# Hint button
|
||||||
|
self.hint_button = ctk.CTkButton(
|
||||||
|
self.app,
|
||||||
|
text="Hint",
|
||||||
|
command=self.show_hint,
|
||||||
|
fg_color="white",
|
||||||
|
hover_color="white",
|
||||||
|
text_color="black",
|
||||||
|
font=("Arial", 20)
|
||||||
|
)
|
||||||
|
self.hint_button.pack(pady=10)
|
||||||
|
|
||||||
|
# Feedback label (centered — matches web layout)
|
||||||
|
self.feedback_label = ctk.CTkLabel(
|
||||||
|
self.app,
|
||||||
|
text="",
|
||||||
|
font=("Arial", 20),
|
||||||
|
wraplength=550,
|
||||||
|
anchor="center"
|
||||||
|
)
|
||||||
|
self.feedback_label.pack(pady=20, padx=10, fill="x")
|
||||||
|
|
||||||
|
# Next Round button
|
||||||
|
self.next_round_button = ctk.CTkButton(
|
||||||
|
self.app,
|
||||||
|
text="Next Round",
|
||||||
|
command=self.start_new_round,
|
||||||
|
fg_color="white",
|
||||||
|
hover_color="white",
|
||||||
|
text_color="black",
|
||||||
|
font=("Arial", 20)
|
||||||
|
)
|
||||||
|
self.next_round_button.pack(pady=10)
|
||||||
|
self.next_round_button.configure(state="disabled")
|
||||||
|
|
||||||
|
# Exit button
|
||||||
|
self.exit_button = ctk.CTkButton(
|
||||||
|
self.app,
|
||||||
|
text="Exit",
|
||||||
|
command=self._exit,
|
||||||
|
fg_color="#555",
|
||||||
|
text_color="#ccc",
|
||||||
|
font=("Arial", 16),
|
||||||
|
hover_color="#666"
|
||||||
|
)
|
||||||
|
self.exit_button.pack(pady=(4, 10))
|
||||||
|
|
||||||
# Notifications
|
# Notifications
|
||||||
self.notifications = NotificationSystem(self.app)
|
self.notifications = NotificationSystem(self.app)
|
||||||
|
|
||||||
# Hint system
|
# Hint system
|
||||||
self.hints = HintSystem(self.app)
|
self.hints = HintSystem(self.app)
|
||||||
self.hints.set_hint_button(self.buttons.hint_button)
|
self.hints.set_hint_button(self.hint_button)
|
||||||
|
|
||||||
# Stats display
|
# Stats display
|
||||||
self.stats = StatsDisplay(self.app)
|
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):
|
def start_new_round(self):
|
||||||
"""Start a new round of the game"""
|
"""Start a new round of the game"""
|
||||||
self._reset_ui()
|
self._reset_ui()
|
||||||
@ -60,11 +98,12 @@ class BlackjackGUI:
|
|||||||
def _reset_ui(self):
|
def _reset_ui(self):
|
||||||
"""Reset UI for new round"""
|
"""Reset UI for new round"""
|
||||||
self.hints._close_hint_window()
|
self.hints._close_hint_window()
|
||||||
self.buttons.hint_button.configure(state="normal")
|
self.hint_button.configure(state="normal")
|
||||||
self.feedback_label.configure(text="")
|
self.feedback_label.configure(text="")
|
||||||
self.buttons.next_round_button.configure(state="disabled")
|
self.next_round_button.configure(state="disabled")
|
||||||
self.buttons._enable_action_buttons(True)
|
self.buttons._enable_action_buttons(True)
|
||||||
self.buttons._reset_button_colors()
|
self.buttons._reset_button_colors()
|
||||||
|
self.buttons.split_button.configure(text="")
|
||||||
|
|
||||||
if hasattr(self.notifications, 'streak_label') and self.notifications.streak_label:
|
if hasattr(self.notifications, 'streak_label') and self.notifications.streak_label:
|
||||||
self.notifications.streak_label.configure(text="")
|
self.notifications.streak_label.configure(text="")
|
||||||
@ -84,18 +123,18 @@ class BlackjackGUI:
|
|||||||
text_color="green"
|
text_color="green"
|
||||||
)
|
)
|
||||||
self.buttons._enable_action_buttons(False)
|
self.buttons._enable_action_buttons(False)
|
||||||
self.buttons.next_round_button.configure(state="normal")
|
self.next_round_button.configure(state="normal")
|
||||||
|
|
||||||
# Handle split possibility
|
# Handle split possibility
|
||||||
can_split_bool = can_split(self.game_logic.player_hand)
|
can_split_bool = can_split(self.game_logic.player_hand)
|
||||||
if can_split_bool:
|
if can_split_bool:
|
||||||
self.buttons.split_button.configure(state="normal")
|
self.buttons.split_button.configure(state="normal", text="Split")
|
||||||
self.feedback_label.configure(
|
self.feedback_label.configure(
|
||||||
text="You have a pair! Consider splitting.",
|
text="You have a pair! Consider splitting.",
|
||||||
text_color="white"
|
text_color="white"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.buttons.split_button.configure(state="disabled")
|
self.buttons.split_button.configure(state="disabled", text="")
|
||||||
|
|
||||||
# Update notifications
|
# Update notifications
|
||||||
stats = self.game_logic.get_stats()
|
stats = self.game_logic.get_stats()
|
||||||
@ -147,12 +186,80 @@ class BlackjackGUI:
|
|||||||
|
|
||||||
# Update button states
|
# Update button states
|
||||||
self.buttons._enable_action_buttons(False)
|
self.buttons._enable_action_buttons(False)
|
||||||
self.buttons.next_round_button.configure(state="normal")
|
self.next_round_button.configure(state="normal")
|
||||||
|
|
||||||
def _on_main_window_close(self):
|
def _on_main_window_close(self):
|
||||||
"""Handle window close event"""
|
"""Handle window close event — just close directly"""
|
||||||
self.app.configure(state="disabled")
|
self.app.destroy()
|
||||||
self.stats._show_final_stats(self.game_logic.get_stats())
|
|
||||||
|
def _create_start_screen(self):
|
||||||
|
"""Create the start screen overlay"""
|
||||||
|
if hasattr(self, 'start_frame') and self.start_frame and self.start_frame.winfo_exists():
|
||||||
|
return
|
||||||
|
self.start_frame = ctk.CTkFrame(self.app, fg_color="#2b2b2b", corner_radius=0)
|
||||||
|
self.start_frame.place(relx=0, rely=0, relwidth=1, relheight=1)
|
||||||
|
|
||||||
|
content = ctk.CTkFrame(self.start_frame, fg_color="transparent")
|
||||||
|
content.place(relx=0.5, rely=0.5, anchor="center")
|
||||||
|
|
||||||
|
ctk.CTkLabel(
|
||||||
|
content,
|
||||||
|
text="\U0001F0CF Blackjack Theory Trainer",
|
||||||
|
font=("Arial", 28, "bold"),
|
||||||
|
text_color="white"
|
||||||
|
).pack(pady=(0, 20))
|
||||||
|
|
||||||
|
ctk.CTkLabel(
|
||||||
|
content,
|
||||||
|
text="This is a blackjack basic strategy trainer and not a real blackjack game.\n\nEach round you're dealt a hand and asked to make the correct play (Hit, Stand, Double, or Split) according to basic strategy. You get immediate feedback and track your accuracy over time.\n\nHave Fun!",
|
||||||
|
font=("Arial", 15),
|
||||||
|
text_color="#ccc",
|
||||||
|
wraplength=450,
|
||||||
|
justify="center"
|
||||||
|
).pack(pady=(0, 30))
|
||||||
|
|
||||||
|
ctk.CTkButton(
|
||||||
|
content,
|
||||||
|
text="Start Training",
|
||||||
|
command=self._start_training,
|
||||||
|
fg_color="#2aa44f",
|
||||||
|
hover_color="#238c44",
|
||||||
|
text_color="white",
|
||||||
|
font=("Arial", 20, "bold"),
|
||||||
|
height=45,
|
||||||
|
width=200
|
||||||
|
).pack(pady=(0, 10))
|
||||||
|
|
||||||
|
ctk.CTkButton(
|
||||||
|
content,
|
||||||
|
text="Quit",
|
||||||
|
command=self._on_main_window_close,
|
||||||
|
fg_color="#555",
|
||||||
|
text_color="#ccc",
|
||||||
|
font=("Arial", 16),
|
||||||
|
height=35,
|
||||||
|
width=120,
|
||||||
|
hover_color="#666"
|
||||||
|
).pack()
|
||||||
|
|
||||||
|
def _start_training(self):
|
||||||
|
"""Start button handler — enter game mode"""
|
||||||
|
if hasattr(self, 'start_frame') and self.start_frame:
|
||||||
|
self.start_frame.destroy()
|
||||||
|
self.start_frame = None
|
||||||
|
self.start_new_round()
|
||||||
|
|
||||||
|
def _exit(self):
|
||||||
|
"""Exit button handler — show stats popup"""
|
||||||
|
self.buttons._enable_action_buttons(False)
|
||||||
|
self.next_round_button.configure(state="disabled")
|
||||||
|
self.hint_button.configure(state="disabled")
|
||||||
|
self.stats._show_exit_popup(self.game_logic.get_stats(), self._exit_to_start)
|
||||||
|
|
||||||
|
def _exit_to_start(self):
|
||||||
|
"""Reset game state and return to start screen"""
|
||||||
|
self.game_logic = BlackjackGame()
|
||||||
|
self._create_start_screen()
|
||||||
|
|
||||||
def show_hint(self):
|
def show_hint(self):
|
||||||
"""Delegate hint display to the HintSystem"""
|
"""Delegate hint display to the HintSystem"""
|
||||||
|
|||||||
@ -18,20 +18,48 @@ body{background:#2b2b2b;color:#fff;font-family:Arial,sans-serif;display:flex;jus
|
|||||||
#actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%;max-width:340px;margin:16px 0 8px}
|
#actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%;max-width:340px;margin:16px 0 8px}
|
||||||
.action-btn{padding:12px;font-size:20px;border:none;border-radius:8px;cursor:pointer;background:#fff;color:#000;font-weight:700;transition:background .15s,color .15s}
|
.action-btn{padding:12px;font-size:20px;border:none;border-radius:8px;cursor:pointer;background:#fff;color:#000;font-weight:700;transition:background .15s,color .15s}
|
||||||
.action-btn:disabled{background:#555;color:#999;cursor:not-allowed}
|
.action-btn:disabled{background:#555;color:#999;cursor:not-allowed}
|
||||||
#btn-hint{background:#fff;color:#000;font-size:20px;border:none;border-radius:8px;padding:12px 40px;cursor:pointer;margin:8px 0;font-weight:700}
|
.btn-util{background:#fff;color:#000;font-size:20px;border:none;border-radius:8px;padding:12px 20px;cursor:pointer;margin:8px 0;font-weight:700;width:220px}
|
||||||
#btn-hint:disabled{background:#555;color:#999;cursor:not-allowed}
|
.btn-util:disabled{background:#555;color:#999;cursor:not-allowed}
|
||||||
#feedback{font-size:20px;text-align:center;min-height:60px;padding:10px;width:100%;max-width:550px;line-height:1.4;word-wrap:break-word}
|
#feedback{font-size:20px;text-align:center;min-height:60px;padding:10px;width:100%;max-width:550px;line-height:1.4;word-wrap:break-word}
|
||||||
#btn-next{background:#fff;color:#000;font-size:20px;border:none;border-radius:8px;padding:12px 40px;cursor:pointer;margin:8px 0;font-weight:700}
|
|
||||||
#btn-next:disabled{background:#555;color:#999;cursor:not-allowed}
|
|
||||||
#toast{position:fixed;top:20px;right:20px;background:#3a7ebf;color:#fff;padding:12px 24px;border-radius:8px;font-size:16px;font-weight:700;opacity:0;transform:translateY(-20px);transition:opacity .3s,transform .3s;pointer-events:none;z-index:1000}
|
#toast{position:fixed;top:20px;right:20px;background:#3a7ebf;color:#fff;padding:12px 24px;border-radius:8px;font-size:16px;font-weight:700;opacity:0;transform:translateY(-20px);transition:opacity .3s,transform .3s;pointer-events:none;z-index:1000}
|
||||||
#toast.show{opacity:1;transform:translateY(0)}
|
#toast.show{opacity:1;transform:translateY(0)}
|
||||||
#lightbox{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);justify-content:center;align-items:center;z-index:999;cursor:pointer}
|
#lightbox{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);justify-content:center;align-items:center;z-index:999;cursor:pointer}
|
||||||
#lightbox.open{display:flex}
|
#lightbox.open{display:flex}
|
||||||
#lightbox img{max-width:90vw;max-height:90vh;border-radius:8px}
|
#lightbox img{max-width:90vw;max-height:90vh;border-radius:8px}
|
||||||
|
#start-screen{display:flex;justify-content:center;align-items:center;min-height:80vh;width:100%}
|
||||||
|
#start-content{text-align:center;max-width:500px}
|
||||||
|
#start-content h1{font-size:32px;margin-bottom:24px;color:#fff}
|
||||||
|
#start-content p{font-size:16px;line-height:1.6;color:#ccc;margin-bottom:32px}
|
||||||
|
#btn-start{background:#2aa44f;color:#fff;font-size:22px;border:none;border-radius:8px;padding:14px 48px;cursor:pointer;font-weight:700}
|
||||||
|
#btn-start:hover{background:#238c44}
|
||||||
|
#game-screen{width:100%}
|
||||||
|
#stats-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);justify-content:center;align-items:center;z-index:999}
|
||||||
|
#stats-modal.open{display:flex}
|
||||||
|
#stats-content{background:#2b2b2b;padding:32px;border-radius:12px;max-width:450px;width:90%;text-align:center}
|
||||||
|
#stats-content h2{font-size:24px;margin-bottom:20px;color:#fff}
|
||||||
|
#stats-data{text-align:left;margin-bottom:24px}
|
||||||
|
.stat-row{display:flex;justify-content:space-between;padding:6px 0;font-size:16px;color:#ccc;border-bottom:1px solid #444}
|
||||||
|
.stat-row:last-child{border-bottom:none}
|
||||||
|
.stat-label{color:#aaa}
|
||||||
|
.stat-value{color:#fff;font-weight:700}
|
||||||
|
.scenario-header{font-size:16px;font-weight:700;color:#fff;margin-top:16px;margin-bottom:8px}
|
||||||
|
#btn-back-to-start{background:#2aa44f;color:#fff;font-size:18px;border:none;border-radius:8px;padding:12px 36px;cursor:pointer;font-weight:700;margin-top:8px}
|
||||||
|
#btn-back-to-start:hover{background:#238c44}
|
||||||
|
#btn-exit{background:#555;color:#ccc;margin-top:4px}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
<div id="start-screen">
|
||||||
|
<div id="start-content">
|
||||||
|
<h1>🃏 Blackjack Theory Trainer</h1>
|
||||||
|
<p>This is a blackjack basic strategy trainer and not a real blackjack game.</p>
|
||||||
|
<p>Each round you're dealt a hand and asked to make the correct play (Hit, Stand, Double, or Split) according to basic strategy. You get immediate feedback and track your accuracy over time.</p>
|
||||||
|
<p>Have Fun!</p>
|
||||||
|
<button id="btn-start">Start Training</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="game-screen">
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<span id="streak"></span>
|
<span id="streak"></span>
|
||||||
</div>
|
</div>
|
||||||
@ -50,9 +78,18 @@ body{background:#2b2b2b;color:#fff;font-family:Arial,sans-serif;display:flex;jus
|
|||||||
<button class="action-btn" id="btn-double">Double</button>
|
<button class="action-btn" id="btn-double">Double</button>
|
||||||
<button class="action-btn" id="btn-split">Split</button>
|
<button class="action-btn" id="btn-split">Split</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-hint">Hint</button>
|
<button id="btn-hint" class="btn-util">Hint</button>
|
||||||
<div id="feedback"></div>
|
<div id="feedback"></div>
|
||||||
<button id="btn-next" disabled>Next Round</button>
|
<button id="btn-next" class="btn-util" disabled>Next Round</button>
|
||||||
|
<button id="btn-exit" class="btn-util">Exit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="stats-modal">
|
||||||
|
<div id="stats-content">
|
||||||
|
<h2>Your Statistics</h2>
|
||||||
|
<div id="stats-data"></div>
|
||||||
|
<button id="btn-back-to-start">Back to Start</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
@ -102,7 +139,7 @@ function markButton(btn,correct){
|
|||||||
btn.style.color='#fff';
|
btn.style.color='#fff';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCards(dc,hc,isNewRound){
|
function renderCards(dc,hc){
|
||||||
dealerCards.innerHTML='';
|
dealerCards.innerHTML='';
|
||||||
playerCards.innerHTML='';
|
playerCards.innerHTML='';
|
||||||
var d=document.createElement('img');
|
var d=document.createElement('img');
|
||||||
@ -113,34 +150,30 @@ function renderCards(dc,hc,isNewRound){
|
|||||||
i.src=cardUrl(c);i.alt='Player card';
|
i.src=cardUrl(c);i.alt='Player card';
|
||||||
playerCards.appendChild(i);
|
playerCards.appendChild(i);
|
||||||
});
|
});
|
||||||
if(isNewRound){
|
|
||||||
feedback.textContent='';
|
|
||||||
if(hc===undefined){
|
|
||||||
btnHint.disabled=false;
|
|
||||||
btnNext.disabled=true;
|
|
||||||
enableActions(true);
|
|
||||||
resetButtonColors();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startRound(){
|
async function startRound(){
|
||||||
try{
|
try{
|
||||||
var r=await fetch('/api/start-round',{method:'POST'});
|
var r=await fetch('/api/start-round',{method:'POST'});
|
||||||
var d=await r.json();
|
var d=await r.json();
|
||||||
renderCards(d.dealer_card,d.player_hand,true);
|
renderCards(d.dealer_card,d.player_hand);
|
||||||
|
feedback.textContent='';
|
||||||
setStreak(d.current_streak);
|
setStreak(d.current_streak);
|
||||||
btnHint.disabled=false;
|
btnHint.disabled=false;
|
||||||
|
btnNext.disabled=true;
|
||||||
enableActions(true);
|
enableActions(true);
|
||||||
resetButtonColors();
|
resetButtonColors();
|
||||||
|
btnSplit.textContent=d.can_split?'Split':'';
|
||||||
|
btnSplit.disabled=!d.can_split;
|
||||||
if(d.has_blackjack){
|
if(d.has_blackjack){
|
||||||
feedback.textContent='Blackjack! You win or get a push!';
|
feedback.textContent='Blackjack! You win or get a push!';
|
||||||
feedback.style.color='green';
|
feedback.style.color='green';
|
||||||
enableActions(false);
|
enableActions(false);
|
||||||
btnNext.disabled=false;
|
btnNext.disabled=false;
|
||||||
|
}else if(d.can_split){
|
||||||
|
feedback.textContent='You have a pair! Consider splitting.';
|
||||||
|
feedback.style.color='white';
|
||||||
}
|
}
|
||||||
if(d.can_split){btnSplit.disabled=false}
|
|
||||||
else{btnSplit.disabled=true}
|
|
||||||
}catch(e){feedback.textContent='Error connecting to server';feedback.style.color='red'}
|
}catch(e){feedback.textContent='Error connecting to server';feedback.style.color='red'}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +204,67 @@ btnNext.addEventListener('click',startRound);
|
|||||||
btnHint.addEventListener('click',function(){lightbox.classList.add('open');btnHint.disabled=true});
|
btnHint.addEventListener('click',function(){lightbox.classList.add('open');btnHint.disabled=true});
|
||||||
lightbox.addEventListener('click',function(){lightbox.classList.remove('open')});
|
lightbox.addEventListener('click',function(){lightbox.classList.remove('open')});
|
||||||
|
|
||||||
startRound();
|
// Screen management
|
||||||
|
var startScreen=document.getElementById('start-screen');
|
||||||
|
var gameScreen=document.getElementById('game-screen');
|
||||||
|
var statsModal=document.getElementById('stats-modal');
|
||||||
|
var statsData=document.getElementById('stats-data');
|
||||||
|
var btnStart=document.getElementById('btn-start');
|
||||||
|
var btnExit=document.getElementById('btn-exit');
|
||||||
|
var btnBackToStart=document.getElementById('btn-back-to-start');
|
||||||
|
|
||||||
|
function showStartScreen(){
|
||||||
|
startScreen.style.display='flex';
|
||||||
|
gameScreen.style.display='none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGameScreen(){
|
||||||
|
startScreen.style.display='none';
|
||||||
|
gameScreen.style.display='block';
|
||||||
|
startRound();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCookie(name){
|
||||||
|
document.cookie=name+'=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showStats(){
|
||||||
|
try{
|
||||||
|
var r=await fetch('/api/stats');
|
||||||
|
if(!r.ok)return;
|
||||||
|
var s=await r.json();
|
||||||
|
var total=s.correct_decisions+s.incorrect_decisions;
|
||||||
|
var acc=total>0?(s.correct_decisions/total*100).toFixed(1):0;
|
||||||
|
var html='';
|
||||||
|
html+='<div class="stat-row"><span class="stat-label">Total Rounds</span><span class="stat-value">'+s.total_rounds+'</span></div>';
|
||||||
|
html+='<div class="stat-row"><span class="stat-label">Accuracy</span><span class="stat-value">'+acc+'%</span></div>';
|
||||||
|
html+='<div class="stat-row"><span class="stat-label">Current Streak</span><span class="stat-value">'+s.current_streak+'</span></div>';
|
||||||
|
html+='<div class="stat-row"><span class="stat-label">Longest Streak</span><span class="stat-value">'+s.longest_streak+'</span></div>';
|
||||||
|
html+='<div class="scenario-header">Scenario Breakdown</div>';
|
||||||
|
for(var key in s.by_scenario){
|
||||||
|
var d=s.by_scenario[key];
|
||||||
|
var t=d.correct+d.incorrect;
|
||||||
|
var a=t>0?(d.correct/t*100).toFixed(1):0;
|
||||||
|
var label=key.replace(/_/g,' ').replace(/\b\w/g,function(l){return l.toUpperCase()});
|
||||||
|
html+='<div class="stat-row"><span class="stat-label">'+label+'</span><span class="stat-value">'+d.correct+'/'+t+' ('+a+'%)</span></div>';
|
||||||
|
}
|
||||||
|
statsData.innerHTML=html;
|
||||||
|
statsModal.classList.add('open');
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToStart(){
|
||||||
|
statsModal.classList.remove('open');
|
||||||
|
deleteCookie(COOKIE);
|
||||||
|
showStartScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
btnStart.addEventListener('click',showGameScreen);
|
||||||
|
btnExit.addEventListener('click',showStats);
|
||||||
|
btnBackToStart.addEventListener('click',backToStart);
|
||||||
|
|
||||||
|
// Start on the start screen
|
||||||
|
showStartScreen();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user