""" ui/predictor_ui.py ------------------ Prediction generator screen — four tabs inside a ttk.Notebook. Generate tab — pick strategy + game + count → generate tickets → save to DB Saved tab — browse all saved predictions, see match count vs last draw Check Ticket tab — compare a user ticket against all historical draws Wheel tab — full-cover wheeling: pick N numbers → all C(N,k) tickets """ import os import tkinter as tk from tkinter import ttk, filedialog, messagebox import logging from db.models import ( get_all_games, get_game_by_name, get_last_draw, insert_prediction, get_predictions, delete_prediction, delete_all_predictions, ) from core.predictor import ( hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, ensemble, quick_pick, ) from core.checker import check_ticket, parse_numbers from core.wheeling import wheel_full, wheel_count, MAX_TICKETS from core.exporter import export_predictions_excel, export_predictions_csv, ensure_exports_dir from ui.widgets import BallsBar logger = logging.getLogger(__name__) _STRATEGIES = { "Ensemble": ensemble, "Hot Numbers": hot_numbers, "Due Numbers": due_numbers, "Weighted Random": weighted_random, "Monte Carlo": monte_carlo, "Positional": positional_pick, "Quick Pick": quick_pick, } _DESCRIPTIONS = { "Ensemble": "Runs all 5 strategies and picks numbers with the most cross-strategy votes.", "Hot Numbers": "Top 5 most frequent numbers from the last 100 draws (recency-weighted).", "Due Numbers": "Numbers most overdue based on expected frequency gap.", "Weighted Random": "Random pick weighted by recency-adjusted historical frequency.", "Monte Carlo": "10,000 simulated draws — pick the most often-selected numbers.", "Positional": "Most frequent number at each draw position (1–5).", "Quick Pick": "Pure random selection — no historical data required.", } _TICKET_COUNTS = [str(n) for n in range(1, 11)] def _parse_raw_numbers(raw: str) -> list[int]: """Parse space- or comma-separated integers from a string, ignoring non-numeric tokens.""" result = [] for tok in raw.replace(",", " ").split(): try: result.append(int(tok)) except ValueError: pass return result def _count_matches(pred_numbers: str, last_draw) -> str: """Compare prediction numbers to last draw, return 'X/5' string.""" if last_draw is None: return "—" try: pred_set = {int(n) for n in pred_numbers.split(",")} draw_set = {int(n) for n in last_draw["numbers"].split(",")} matched = len(pred_set & draw_set) return f"{matched}/{len(pred_set)}" except Exception: return "—" class PredictorScreen(ttk.Frame): def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) self._game_id: int | None = None self._tickets: list[dict] = [] self._strategy_name: str = "Hot Numbers" self._wheel_tickets: list[list[int]] = [] self._build_ui() # ── UI construction ─────────────────────────────────────────────────────── def _build_ui(self): nb = ttk.Notebook(self) nb.pack(fill="both", expand=True) gen_frame = ttk.Frame(nb) saved_frame = ttk.Frame(nb) check_frame = ttk.Frame(nb) wheel_frame = ttk.Frame(nb) nb.add(gen_frame, text="Generate") nb.add(saved_frame, text="Saved") nb.add(check_frame, text="Check Ticket") nb.add(wheel_frame, text="Wheel") self._build_generate_tab(gen_frame) self._build_saved_tab(saved_frame) self._build_check_tab(check_frame) self._build_wheel_tab(wheel_frame) # ── Generate tab ────────────────────────────────────────────────────────── def _build_generate_tab(self, parent): bar = ttk.Frame(parent, padding=(6, 6, 6, 4)) bar.pack(fill="x") ttk.Label(bar, text="Game:").pack(side="left") self._game_var = tk.StringVar() self._game_cb = ttk.Combobox( bar, textvariable=self._game_var, state="readonly", width=15 ) self._game_cb.pack(side="left", padx=(4, 14)) self._game_cb.bind("<>", lambda _: self._on_game_change()) ttk.Label(bar, text="Strategy:").pack(side="left") self._strategy_var = tk.StringVar(value="Hot Numbers") strategy_cb = ttk.Combobox( bar, textvariable=self._strategy_var, values=list(_STRATEGIES.keys()), state="readonly", width=16, ) strategy_cb.pack(side="left", padx=(4, 14)) strategy_cb.bind("<>", lambda _: self._on_strategy_change()) ttk.Label(bar, text="Tickets:").pack(side="left") self._count_var = tk.StringVar(value="1") ttk.Combobox( bar, textvariable=self._count_var, values=_TICKET_COUNTS, state="readonly", width=4, ).pack(side="left", padx=(4, 0)) ttk.Label(bar, text="Exclude:", padding=(12, 0, 0, 0)).pack(side="left") self._exclude_var = tk.StringVar() ttk.Entry(bar, textvariable=self._exclude_var, width=14).pack(side="left", padx=(4, 0)) self._gen_btn = ttk.Button(bar, text="Generate", command=self._generate) self._gen_btn.pack(side="left", padx=(12, 0)) # Results treeview tree_frame = ttk.Frame(parent) tree_frame.pack(fill="both", expand=True, padx=6, pady=(4, 0)) cols = ("#", "numbers", "bonus") self._tree = ttk.Treeview( tree_frame, columns=cols, show="headings", selectmode="browse" ) self._tree.heading("#", text="#") self._tree.heading("numbers", text="Numbers") self._tree.heading("bonus", text="Bonus") self._tree.column("#", width=40, anchor="center", stretch=False) self._tree.column("numbers", width=280, anchor="w") self._tree.column("bonus", width=70, anchor="center", stretch=False) vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self._tree.yview) self._tree.configure(yscrollcommand=vsb.set) self._tree.pack(side="left", fill="both", expand=True) vsb.pack(side="right", fill="y") self._tree.bind("<>", self._on_gen_row_select) # ── Ball detail strip ───────────────────────────────────────────────── ttk.Separator(parent, orient="horizontal").pack(fill="x", padx=6) self._gen_detail = ttk.Frame(parent, padding=(8, 3)) self._gen_detail.pack(fill="x") # Bottom bar bottom = ttk.Frame(parent, padding=(6, 4)) bottom.pack(fill="x") self._desc_var = tk.StringVar(value=_DESCRIPTIONS["Hot Numbers"]) ttk.Label( bottom, textvariable=self._desc_var, foreground="#555555", anchor="w", ).pack(side="left", fill="x", expand=True) self._status_var = tk.StringVar() ttk.Label(bottom, textvariable=self._status_var, foreground="#27ae60").pack(side="left", padx=(8, 0)) self._save_btn = ttk.Button( bottom, text="Save to DB", command=self._save, state="disabled" ) self._save_btn.pack(side="right") self._copy_btn = ttk.Button( bottom, text="Copy", command=self._copy_tickets, state="disabled" ) self._copy_btn.pack(side="right", padx=(0, 4)) ttk.Button(bottom, text="Clear", command=self._clear ).pack(side="right", padx=(0, 4)) ttk.Button(bottom, text="Export CSV", command=self._export_csv ).pack(side="right", padx=(0, 4)) ttk.Button(bottom, text="Export Excel", command=self._export_excel ).pack(side="right", padx=(0, 4)) # ── Saved tab ───────────────────────────────────────────────────────────── def _build_saved_tab(self, parent): # Filter + action bar bar = ttk.Frame(parent, padding=(6, 6, 6, 4)) bar.pack(fill="x") ttk.Label(bar, text="Game:").pack(side="left") self._saved_game_var = tk.StringVar(value="All Games") self._saved_game_cb = ttk.Combobox( bar, textvariable=self._saved_game_var, state="readonly", width=15 ) self._saved_game_cb.pack(side="left", padx=(4, 14)) self._saved_game_cb.bind("<>", lambda _: self._refresh_saved()) ttk.Button(bar, text="↻ Refresh", command=self._refresh_saved).pack(side="left", padx=(0, 4)) ttk.Button(bar, text="Delete Selected", command=self._delete_selected).pack(side="left", padx=(0, 4)) ttk.Button(bar, text="Clear All", command=self._clear_all_saved).pack(side="left") self._saved_count_var = tk.StringVar(value="0 saved") ttk.Label(bar, textvariable=self._saved_count_var, foreground="#777777").pack(side="right") # Treeview tree_frame = ttk.Frame(parent) tree_frame.pack(fill="both", expand=True, padx=6, pady=(2, 6)) s_cols = ("id", "game", "strategy", "numbers", "bonus", "matches", "saved") self._saved_tree = ttk.Treeview( tree_frame, columns=s_cols, show="headings", selectmode="extended" ) self._saved_tree.heading("id", text="ID") self._saved_tree.heading("game", text="Game") self._saved_tree.heading("strategy", text="Strategy") self._saved_tree.heading("numbers", text="Numbers") self._saved_tree.heading("bonus", text="Bonus") self._saved_tree.heading("matches", text="Matches") self._saved_tree.heading("saved", text="Saved") self._saved_tree.column("id", width=45, anchor="center", stretch=False) self._saved_tree.column("game", width=120, anchor="w", stretch=False) self._saved_tree.column("strategy", width=120, anchor="w", stretch=False) self._saved_tree.column("numbers", width=220, anchor="w") self._saved_tree.column("bonus", width=55, anchor="center", stretch=False) self._saved_tree.column("matches", width=65, anchor="center", stretch=False) self._saved_tree.column("saved", width=130, anchor="center", stretch=False) vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self._saved_tree.yview) hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self._saved_tree.xview) self._saved_tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) self._saved_tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") tree_frame.rowconfigure(0, weight=1) tree_frame.columnconfigure(0, weight=1) # Colour rows by match count self._saved_tree.tag_configure("match_good", foreground="#1e8449") self._saved_tree.tag_configure("match_zero", foreground="#aaaaaa") # ── Refresh / data ──────────────────────────────────────────────────────── def refresh(self): self._load_games() self._load_check_games() self._load_wheel_games() self._refresh_saved() def _load_games(self): games = get_all_games(active_only=True) names = [g["name"] for g in games] self._game_cb["values"] = names if not self._game_var.get() or self._game_var.get() not in names: if names: self._game_var.set(names[0]) game = get_game_by_name(self._game_var.get()) self._game_id = game["id"] if game else None saved_names = ["All Games"] + names self._saved_game_cb["values"] = saved_names if self._saved_game_var.get() not in saved_names: self._saved_game_var.set("All Games") def _refresh_saved(self): name = self._saved_game_var.get() game_id = None if name != "All Games": g = get_game_by_name(name) game_id = g["id"] if g else None preds = get_predictions(game_id=game_id, limit=100_000) # Cache last draw per game to avoid repeated DB hits last_draw_cache: dict[int, object] = {} self._saved_tree.delete(*self._saved_tree.get_children()) for p in preds: gid = p["game_id"] if gid not in last_draw_cache: last_draw_cache[gid] = get_last_draw(gid) last = last_draw_cache[gid] match_str = _count_matches(p["numbers"], last) nums_fmt = " ".join(f"{int(n):02d}" for n in p["numbers"].split(",")) saved_ts = p["created_at"][:16].replace("T", " ") try: matched_n = int(match_str.split("/")[0]) tag = "match_good" if matched_n > 0 else "match_zero" except Exception: tag = "" self._saved_tree.insert("", "end", iid=str(p["id"]), values=( p["id"], p["game_name"], p["strategy"], nums_fmt, p["bonus"] or "—", match_str, saved_ts, ), tags=(tag,), ) count = len(preds) self._saved_count_var.set(f"{count} saved prediction{'s' if count != 1 else ''}") # ── Generate tab callbacks ──────────────────────────────────────────────── def _on_game_change(self): game = get_game_by_name(self._game_var.get()) self._game_id = game["id"] if game else None self._clear() def _on_strategy_change(self): self._strategy_name = self._strategy_var.get() self._desc_var.set(_DESCRIPTIONS.get(self._strategy_name, "")) self._clear() def _generate(self): if self._game_id is None: self._status_var.set("Select a game first.") return strategy_fn = _STRATEGIES[self._strategy_var.get()] count = int(self._count_var.get()) self._clear() self._gen_btn.config(state="disabled", text="Generating…") self.update_idletasks() try: excl = self._parse_exclude() tickets = [strategy_fn(self._game_id, exclude=excl) for _ in range(count)] self._tickets = tickets self._display(tickets) self._save_btn.config(state="normal") self._copy_btn.config(state="normal") self._status_var.set(f"{len(tickets)} ticket{'s' if len(tickets) != 1 else ''} generated.") logger.info("[PREDICT] %d ticket(s) generated via %s", count, self._strategy_var.get()) except Exception as e: self._status_var.set(f"Error: {e}") logger.error("[ERROR] Prediction failed: %s", e, exc_info=True) finally: self._gen_btn.config(state="normal", text="Generate") def _display(self, tickets): self._tree.delete(*self._tree.get_children()) game = get_game_by_name(self._game_var.get()) show_bonus = game is not None and game["bonus_count"] > 0 for i, t in enumerate(tickets, start=1): nums_str = " ".join(f"{n:02d}" for n in t["numbers"]) bonus_str = str(t["bonus"]) if show_bonus and t["bonus"] is not None else "—" self._tree.insert("", "end", values=(i, nums_str, bonus_str)) def _save(self): if not self._tickets or self._game_id is None: return strategy_name = self._strategy_var.get() saved = 0 for t in self._tickets: insert_prediction(self._game_id, strategy_name, t["numbers"], t["bonus"]) saved += 1 self._status_var.set(f"Saved {saved} prediction{'s' if saved != 1 else ''} to DB.") self._save_btn.config(state="disabled") self._refresh_saved() logger.info("[PREDICT] Saved %d prediction(s) to DB", saved) def _parse_exclude(self) -> set: raw = self._exclude_var.get().strip() if not raw: return set() result = set() for tok in raw.replace(",", " ").split(): try: result.add(int(tok)) except ValueError: pass return result def _copy_tickets(self): if not self._tickets: return game = get_game_by_name(self._game_var.get()) show_bonus = game is not None and game["bonus_count"] > 0 lines = [] for i, t in enumerate(self._tickets, 1): nums = " ".join(f"{n:02d}" for n in t["numbers"]) line = f"Ticket {i}: {nums}" if show_bonus and t["bonus"] is not None: line += f" + {t['bonus']:02d}" lines.append(line) self.clipboard_clear() self.clipboard_append("\n".join(lines)) self._status_var.set("Copied to clipboard.") def _on_gen_row_select(self, _event=None): for w in self._gen_detail.winfo_children(): w.destroy() sel = self._tree.selection() if not sel: return vals = self._tree.item(sel[0], "values") # vals: (#, numbers_fmt, bonus) try: nums = [int(x) for x in str(vals[1]).split() if x.isdigit()] b_txt = str(vals[2]) bonus = int(b_txt) if b_txt.isdigit() else None except Exception: return if nums: BallsBar(self._gen_detail, numbers=nums, bonus=bonus).pack(anchor="w") def _clear(self): self._tickets = [] self._tree.delete(*self._tree.get_children()) for w in self._gen_detail.winfo_children(): w.destroy() self._save_btn.config(state="disabled") self._copy_btn.config(state="disabled") self._status_var.set("") def _export_excel(self): ensure_exports_dir() fp = filedialog.asksaveasfilename( title="Export predictions", defaultextension=".xlsx", filetypes=[("Excel workbook", "*.xlsx")], initialfile=f"predictions_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.xlsx", ) if not fp: return try: export_predictions_excel(fp, game_id=self._game_id) self._status_var.set(f"Exported → {os.path.basename(fp)}") except Exception as e: messagebox.showerror("Export failed", str(e)) def _export_csv(self): ensure_exports_dir() fp = filedialog.asksaveasfilename( title="Export predictions", defaultextension=".csv", filetypes=[("CSV file", "*.csv")], initialfile=f"predictions_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.csv", ) if not fp: return try: export_predictions_csv(fp, game_id=self._game_id) self._status_var.set(f"Exported → {os.path.basename(fp)}") except Exception as e: messagebox.showerror("Export failed", str(e)) # ── Saved tab callbacks ─────────────────────────────────────────────────── def _delete_selected(self): selected = self._saved_tree.selection() if not selected: return for iid in selected: pred_id = int(self._saved_tree.item(iid, "values")[0]) delete_prediction(pred_id) self._refresh_saved() def _clear_all_saved(self): name = self._saved_game_var.get() game_id = None if name != "All Games": g = get_game_by_name(name) game_id = g["id"] if g else None scope = f" for {name}" if game_id else "" if not messagebox.askyesno("Clear All", f"Delete all saved predictions{scope}?"): return delete_all_predictions(game_id=game_id) self._refresh_saved() # ── Check Ticket tab ────────────────────────────────────────────────────── def _build_check_tab(self, parent): # Input bar bar = ttk.Frame(parent, padding=(6, 8, 6, 4)) bar.pack(fill="x") ttk.Label(bar, text="Game:").pack(side="left") self._chk_game_var = tk.StringVar() self._chk_game_cb = ttk.Combobox( bar, textvariable=self._chk_game_var, state="readonly", width=15 ) self._chk_game_cb.pack(side="left", padx=(4, 14)) ttk.Label(bar, text="Numbers:").pack(side="left") self._chk_nums_var = tk.StringVar() chk_nums_entry = ttk.Entry(bar, textvariable=self._chk_nums_var, width=22) chk_nums_entry.pack(side="left", padx=(4, 4)) chk_nums_entry.bind("", lambda e: self._run_check()) ttk.Label(bar, text="Bonus:").pack(side="left") self._chk_bonus_var = tk.StringVar() chk_bonus_entry = ttk.Entry(bar, textvariable=self._chk_bonus_var, width=5) chk_bonus_entry.pack(side="left", padx=(4, 14)) chk_bonus_entry.bind("", lambda e: self._run_check()) ttk.Button(bar, text="Check", command=self._run_check).pack(side="left", padx=(0, 4)) ttk.Button(bar, text="Clear", command=self._clear_check).pack(side="left") self._chk_status_var = tk.StringVar() ttk.Label(bar, textvariable=self._chk_status_var, foreground="#c0392b").pack(side="left", padx=(12, 0)) # Hint text below bar hint = ttk.Frame(parent, padding=(6, 0, 6, 4)) hint.pack(fill="x") ttk.Label(hint, text="Enter space- or comma-separated main numbers, and optionally a bonus number.", foreground="#888888", font=("TkDefaultFont", 8)).pack(anchor="w") # Results treeview tree_frame = ttk.Frame(parent) tree_frame.pack(fill="both", expand=True, padx=6, pady=(0, 4)) c_cols = ("date", "draw_numbers", "bonus", "main", "bonus_hit", "tier") self._chk_tree = ttk.Treeview( tree_frame, columns=c_cols, show="headings", selectmode="browse" ) self._chk_tree.heading("date", text="Draw Date") self._chk_tree.heading("draw_numbers", text="Draw Numbers") self._chk_tree.heading("bonus", text="Bonus") self._chk_tree.heading("main", text="Main Hits") self._chk_tree.heading("bonus_hit", text="Bonus Hit") self._chk_tree.heading("tier", text="Prize Tier") self._chk_tree.column("date", width=100, anchor="center", stretch=False) self._chk_tree.column("draw_numbers", width=190, anchor="w") self._chk_tree.column("bonus", width=55, anchor="center", stretch=False) self._chk_tree.column("main", width=70, anchor="center", stretch=False) self._chk_tree.column("bonus_hit", width=70, anchor="center", stretch=False) self._chk_tree.column("tier", width=160, anchor="w", stretch=False) vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self._chk_tree.yview) hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self._chk_tree.xview) self._chk_tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) self._chk_tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") tree_frame.rowconfigure(0, weight=1) tree_frame.columnconfigure(0, weight=1) # Row colour tags self._chk_tree.tag_configure("jackpot", foreground="#8e44ad", font=("TkDefaultFont", 9, "bold")) self._chk_tree.tag_configure("high", foreground="#1e8449") self._chk_tree.tag_configure("low", foreground="#555555") self._chk_tree.bind("<>", self._on_chk_row_select) # ── Ball detail strip ───────────────────────────────────────────────── ttk.Separator(parent, orient="horizontal").pack(fill="x", padx=6) self._chk_detail = ttk.Frame(parent, padding=(8, 3)) self._chk_detail.pack(fill="x") # Summary label self._chk_summary_var = tk.StringVar() ttk.Label(parent, textvariable=self._chk_summary_var, anchor="e", padding=(6, 2)).pack(fill="x") def _run_check(self): self._chk_status_var.set("") self._chk_tree.delete(*self._chk_tree.get_children()) self._chk_summary_var.set("") # Resolve game game = get_game_by_name(self._chk_game_var.get()) if game is None: self._chk_status_var.set("Select a game first.") return # Parse main numbers try: numbers = parse_numbers(self._chk_nums_var.get()) except ValueError as e: self._chk_status_var.set(str(e)) return if len(numbers) != game["main_count"]: self._chk_status_var.set( f"{game['name']} requires {game['main_count']} main numbers " f"(got {len(numbers)})." ) return # Validate range invalid = [n for n in numbers if not (1 <= n <= game["main_max"])] if invalid: self._chk_status_var.set( f"Numbers out of range 1–{game['main_max']}: {invalid}" ) return # Parse optional bonus bonus = None raw_bonus = self._chk_bonus_var.get().strip() if raw_bonus: if not raw_bonus.isdigit(): self._chk_status_var.set("Bonus must be a number.") return bonus = int(raw_bonus) if game["bonus_max"] and not (1 <= bonus <= game["bonus_max"]): self._chk_status_var.set( f"Bonus out of range 1–{game['bonus_max']}." ) return results = check_ticket(game["id"], numbers, bonus=bonus) for r in results: nums_fmt = " ".join(f"{n:02d}" for n in r["draw_numbers"]) bonus_str = str(r["draw_bonus"]) if r["draw_bonus"] is not None else "—" bonus_hit = "✓" if r["bonus_match"] else "—" tier = r["prize_tier"] if tier == "Jackpot": tag = "jackpot" elif r["main_matches"] >= 3 or r["bonus_match"]: tag = "high" else: tag = "low" self._chk_tree.insert("", "end", values=( r["draw_date"], nums_fmt, bonus_str, r["main_matches"], bonus_hit, tier, ), tags=(tag,)) total = len(results) best = results[0]["prize_tier"] if results else "—" self._chk_summary_var.set( f"{total} draw{'s' if total != 1 else ''} matched • Best: {best}" ) def _on_chk_row_select(self, _event=None): for w in self._chk_detail.winfo_children(): w.destroy() sel = self._chk_tree.selection() if not sel: return vals = self._chk_tree.item(sel[0], "values") # vals: (date, draw_numbers_fmt, bonus, main_hits, bonus_hit, tier) try: draw_nums = [int(x) for x in str(vals[1]).split() if x.isdigit()] db_txt = str(vals[2]) draw_bonus = int(db_txt) if db_txt.isdigit() else None except Exception: return if not draw_nums: return # Ticket numbers from the input entries try: ticket_nums = parse_numbers(self._chk_nums_var.get()) except Exception: ticket_nums = [] raw_b = self._chk_bonus_var.get().strip() ticket_bonus = int(raw_b) if raw_b.isdigit() else None ticket_set = set(ticket_nums) draw_set = set(draw_nums) _GREEN = ("#27ae60", "#ffffff") _GREY = ("#cccccc", "#888888") ticket_hi = {n: (_GREEN if n in draw_set else _GREY) for n in ticket_nums} draw_hi = {n: _GREEN for n in draw_nums if n in ticket_set} if ticket_bonus is not None: ticket_hi[ticket_bonus] = ( _GREEN if draw_bonus is not None and ticket_bonus == draw_bonus else _GREY ) if draw_bonus is not None and ticket_bonus is not None and draw_bonus == ticket_bonus: draw_hi[draw_bonus] = _GREEN grid = ttk.Frame(self._chk_detail) grid.pack(anchor="w") if ticket_nums: ttk.Label(grid, text="Ticket:", foreground="#555555", font=("TkDefaultFont", 8), width=7).grid(row=0, column=0, sticky="w") BallsBar(grid, numbers=ticket_nums, bonus=ticket_bonus, highlights=ticket_hi).grid(row=0, column=1, sticky="w") ttk.Label(grid, text="Draw:", foreground="#555555", font=("TkDefaultFont", 8), width=7).grid(row=1, column=0, sticky="w") BallsBar(grid, numbers=draw_nums, bonus=draw_bonus, highlights=draw_hi).grid(row=1, column=1, sticky="w") def _clear_check(self): self._chk_nums_var.set("") self._chk_bonus_var.set("") self._chk_status_var.set("") self._chk_tree.delete(*self._chk_tree.get_children()) for w in self._chk_detail.winfo_children(): w.destroy() self._chk_summary_var.set("") # ── Wheel tab ───────────────────────────────────────────────────────────── def _build_wheel_tab(self, parent): bar = ttk.Frame(parent, padding=(6, 8, 6, 4)) bar.pack(fill="x") ttk.Label(bar, text="Game:").pack(side="left") self._whl_game_var = tk.StringVar() self._whl_game_cb = ttk.Combobox( bar, textvariable=self._whl_game_var, state="readonly", width=15 ) self._whl_game_cb.pack(side="left", padx=(4, 14)) self._whl_game_cb.bind("<>", lambda _: self._on_wheel_game_change()) ttk.Label(bar, text="Numbers:").pack(side="left") self._whl_nums_var = tk.StringVar() whl_entry = ttk.Entry(bar, textvariable=self._whl_nums_var, width=28) whl_entry.pack(side="left", padx=(4, 4)) whl_entry.bind("", lambda e: self._run_wheel()) self._whl_nums_var.trace_add("write", lambda *_: self._update_wheel_preview()) ttk.Label(bar, text="Pick:").pack(side="left") self._whl_pick_var = tk.IntVar(value=5) self._whl_pick_sb = ttk.Spinbox( bar, from_=1, to=10, textvariable=self._whl_pick_var, width=4, command=self._update_wheel_preview, ) self._whl_pick_sb.pack(side="left", padx=(4, 14)) self._whl_pick_var.trace_add("write", lambda *_: self._update_wheel_preview()) self._whl_btn = ttk.Button(bar, text="Generate Wheel", command=self._run_wheel) self._whl_btn.pack(side="left", padx=(0, 4)) ttk.Button(bar, text="Clear", command=self._clear_wheel).pack(side="left") # Preview + hint hint = ttk.Frame(parent, padding=(6, 0, 6, 4)) hint.pack(fill="x") self._whl_preview_var = tk.StringVar( value=f"Enter more than pick count numbers. Max {MAX_TICKETS} tickets." ) ttk.Label(hint, textvariable=self._whl_preview_var, foreground="#888888", font=("TkDefaultFont", 8)).pack(anchor="w") # Results treeview tree_frame = ttk.Frame(parent) tree_frame.pack(fill="both", expand=True, padx=6, pady=(0, 4)) w_cols = ("#", "numbers") self._whl_tree = ttk.Treeview( tree_frame, columns=w_cols, show="headings", selectmode="browse" ) self._whl_tree.heading("#", text="#") self._whl_tree.heading("numbers", text="Numbers") self._whl_tree.column("#", width=50, anchor="center", stretch=False) self._whl_tree.column("numbers", width=300, anchor="w") vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self._whl_tree.yview) self._whl_tree.configure(yscrollcommand=vsb.set) self._whl_tree.pack(side="left", fill="both", expand=True) vsb.pack(side="right", fill="y") self._whl_tree.bind("<>", self._on_wheel_row_select) # Ball detail strip ttk.Separator(parent, orient="horizontal").pack(fill="x", padx=6) self._whl_detail = ttk.Frame(parent, padding=(8, 3)) self._whl_detail.pack(fill="x") # Bottom bar bottom = ttk.Frame(parent, padding=(6, 4)) bottom.pack(fill="x") self._whl_status_var = tk.StringVar() ttk.Label(bottom, textvariable=self._whl_status_var, foreground="#27ae60").pack(side="left", fill="x", expand=True) self._whl_save_btn = ttk.Button( bottom, text="Save to DB", command=self._save_wheel, state="disabled" ) self._whl_save_btn.pack(side="right") self._whl_copy_btn = ttk.Button( bottom, text="Copy", command=self._copy_wheel, state="disabled" ) self._whl_copy_btn.pack(side="right", padx=(0, 4)) def _load_wheel_games(self): games = get_all_games(active_only=True) names = [g["name"] for g in games] self._whl_game_cb["values"] = names if not self._whl_game_var.get() or self._whl_game_var.get() not in names: if names: self._whl_game_var.set(names[0]) self._on_wheel_game_change() def _on_wheel_game_change(self): game = get_game_by_name(self._whl_game_var.get()) if game: self._whl_pick_var.set(game["main_count"]) self._clear_wheel() self._update_wheel_preview() def _update_wheel_preview(self): raw = self._whl_nums_var.get().strip() nums = _parse_raw_numbers(raw) try: k = int(self._whl_pick_var.get()) except (ValueError, tk.TclError): self._whl_preview_var.set("Invalid pick count.") return if not nums: self._whl_preview_var.set( f"Enter more than pick count numbers. Max {MAX_TICKETS} tickets." ) return n = len(set(nums)) if k < 1 or k > n: self._whl_preview_var.set(f"Pick count must be between 1 and {n}.") return from math import comb count = comb(n, k) if count > MAX_TICKETS: self._whl_preview_var.set( f"Would generate {count:,} tickets — exceeds limit of {MAX_TICKETS}. " f"Reduce pool or pick count." ) else: self._whl_preview_var.set( f"Will generate {count} ticket{'s' if count != 1 else ''} (C({n},{k}))." ) def _run_wheel(self): self._whl_status_var.set("") raw = self._whl_nums_var.get().strip() nums = _parse_raw_numbers(raw) if not nums: self._whl_status_var.set("Enter numbers first.") return game = get_game_by_name(self._whl_game_var.get()) if game is None: self._whl_status_var.set("Select a game first.") return try: k = int(self._whl_pick_var.get()) except (ValueError, tk.TclError): self._whl_status_var.set("Invalid pick count.") return # Validate number ranges bad = [n for n in nums if not (1 <= n <= game["main_max"])] if bad: self._whl_status_var.set( f"Numbers out of range 1–{game['main_max']}: {sorted(set(bad))}" ) return try: tickets = wheel_full(nums, k) except ValueError as e: self._whl_status_var.set(str(e)) return self._wheel_tickets = tickets self._whl_tree.delete(*self._whl_tree.get_children()) for w in self._whl_detail.winfo_children(): w.destroy() for i, combo in enumerate(tickets, 1): nums_str = " ".join(f"{n:02d}" for n in combo) self._whl_tree.insert("", "end", values=(i, nums_str)) self._whl_save_btn.config(state="normal") self._whl_copy_btn.config(state="normal") n_pool = len(set(nums)) self._whl_status_var.set( f"{len(tickets)} ticket{'s' if len(tickets) != 1 else ''} — " f"full cover of {n_pool} numbers pick {k}." ) logger.info("[WHEEL] Generated %d tickets from %d-number pool", len(tickets), n_pool) def _on_wheel_row_select(self, _event=None): for w in self._whl_detail.winfo_children(): w.destroy() sel = self._whl_tree.selection() if not sel: return vals = self._whl_tree.item(sel[0], "values") try: nums = [int(x) for x in str(vals[1]).split() if x.isdigit()] except Exception: return if nums: BallsBar(self._whl_detail, numbers=nums).pack(anchor="w") def _save_wheel(self): if not self._wheel_tickets: return game = get_game_by_name(self._whl_game_var.get()) if game is None: return for combo in self._wheel_tickets: insert_prediction(game["id"], "Wheel", combo, bonus=None) saved = len(self._wheel_tickets) self._whl_status_var.set(f"Saved {saved} ticket{'s' if saved != 1 else ''} to DB.") self._whl_save_btn.config(state="disabled") self._refresh_saved() logger.info("[WHEEL] Saved %d wheel tickets to DB", saved) def _copy_wheel(self): if not self._wheel_tickets: return lines = [ f"Ticket {i}: {' '.join(f'{n:02d}' for n in combo)}" for i, combo in enumerate(self._wheel_tickets, 1) ] self.clipboard_clear() self.clipboard_append("\n".join(lines)) self._whl_status_var.set("Copied to clipboard.") def _clear_wheel(self): self._wheel_tickets = [] if hasattr(self, "_whl_tree"): self._whl_tree.delete(*self._whl_tree.get_children()) if hasattr(self, "_whl_detail"): for w in self._whl_detail.winfo_children(): w.destroy() if hasattr(self, "_whl_save_btn"): self._whl_save_btn.config(state="disabled") if hasattr(self, "_whl_copy_btn"): self._whl_copy_btn.config(state="disabled") if hasattr(self, "_whl_status_var"): self._whl_status_var.set("") def _load_check_games(self): games = get_all_games(active_only=True) names = [g["name"] for g in games] self._chk_game_cb["values"] = names if not self._chk_game_var.get() or self._chk_game_var.get() not in names: if names: self._chk_game_var.set(names[0])