201 lines
8.2 KiB
Python
201 lines
8.2 KiB
Python
"""
|
||
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("<<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("")
|