""" ui/predictor_ui.py ------------------ Prediction generator screen. Pick a strategy + game + ticket count → generate → save to DB. """ import tkinter as tk from tkinter import ttk import logging from db.models import get_all_games, get_game_by_name, insert_prediction from core.predictor import ( hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, ) logger = logging.getLogger(__name__) _STRATEGIES = { "Hot Numbers": hot_numbers, "Due Numbers": due_numbers, "Weighted Random": weighted_random, "Monte Carlo": monte_carlo, "Positional": positional_pick, } _DESCRIPTIONS = { "Hot Numbers": "Top 5 most frequent numbers from the last 100 draws.", "Due Numbers": "Numbers most overdue based on expected frequency gap.", "Weighted Random": "Random pick weighted by each number's historical frequency.", "Monte Carlo": "10,000 simulated draws — pick the most often-selected numbers.", "Positional": "Most frequent number at each draw position (1–5).", } _TICKET_COUNTS = [str(n) for n in range(1, 11)] class PredictorScreen(ttk.Frame): def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) self._game_id: int | None = None self._tickets: list[dict] = [] # [{"numbers": [...], "bonus": int|None}] self._strategy_name: str = "Hot Numbers" self._build_ui() # ── UI construction ─────────────────────────────────────────────────────── def _build_ui(self): # ── Controls bar ────────────────────────────────────────────────────── bar = ttk.Frame(self, 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)) self._gen_btn = ttk.Button(bar, text="Generate", command=self._generate) self._gen_btn.pack(side="left", padx=(14, 0)) # ── Results Treeview ────────────────────────────────────────────────── tree_frame = ttk.Frame(self) 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") # ── Bottom bar ──────────────────────────────────────────────────────── bottom = ttk.Frame(self, 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") ttk.Button( bottom, text="Clear", command=self._clear ).pack(side="right", padx=(0, 4)) # ── Callbacks ───────────────────────────────────────────────────────────── def refresh(self): self._load_games() 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 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: tickets = [strategy_fn(self._game_id) for _ in range(count)] self._tickets = tickets self._display(tickets) self._save_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") logger.info("[PREDICT] Saved %d prediction(s) to DB", saved) def _clear(self): self._tickets = [] self._tree.delete(*self._tree.get_children()) self._save_btn.config(state="disabled") self._status_var.set("")