Files
lottosight/ui/predictor_ui.py
T
2026-05-23 11:32:54 -04:00

201 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 (15).",
}
_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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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("")