05/23 Phase 11

This commit is contained in:
2026-05-23 12:31:20 -04:00
parent f8c11b8979
commit 78e88ae8ab
5 changed files with 412 additions and 27 deletions
+194 -27
View File
@@ -1,8 +1,11 @@
"""
ui/predictor_ui.py
------------------
Prediction generator screen.
Pick a strategy + game + ticket count → generate → save to DB.
Prediction generator screen — two 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 real
draw, delete individual rows or clear all
"""
import os
@@ -10,7 +13,11 @@ import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import logging
from db.models import get_all_games, get_game_by_name, insert_prediction
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,
)
@@ -37,19 +44,45 @@ _DESCRIPTIONS = {
_TICKET_COUNTS = [str(n) for n in range(1, 11)]
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] = [] # [{"numbers": [...], "bonus": int|None}]
self._strategy_name: str = "Hot Numbers"
self._game_id: int | None = None
self._tickets: list[dict] = []
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))
nb = ttk.Notebook(self)
nb.pack(fill="both", expand=True)
gen_frame = ttk.Frame(nb)
saved_frame = ttk.Frame(nb)
nb.add(gen_frame, text="Generate")
nb.add(saved_frame, text="Saved")
self._build_generate_tab(gen_frame)
self._build_saved_tab(saved_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")
@@ -80,8 +113,8 @@ class PredictorScreen(ttk.Frame):
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)
# Results treeview
tree_frame = ttk.Frame(parent)
tree_frame.pack(fill="both", expand=True, padx=6, pady=(4, 0))
cols = ("#", "numbers", "bonus")
@@ -100,8 +133,8 @@ class PredictorScreen(ttk.Frame):
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 bar
bottom = ttk.Frame(parent, padding=(6, 4))
bottom.pack(fill="x")
self._desc_var = tk.StringVar(value=_DESCRIPTIONS["Hot Numbers"])
@@ -119,22 +152,79 @@ class PredictorScreen(ttk.Frame):
)
self._save_btn.pack(side="right")
ttk.Button(
bottom, text="Clear", command=self._clear
).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))
ttk.Button(
bottom, text="Export CSV", command=self._export_csv, state="normal"
).pack(side="right", padx=(0, 4))
# ── Saved tab ─────────────────────────────────────────────────────────────
ttk.Button(
bottom, text="Export Excel", command=self._export_excel, state="normal"
).pack(side="right", padx=(0, 4))
def _build_saved_tab(self, parent):
# Filter + action bar
bar = ttk.Frame(parent, padding=(6, 6, 6, 4))
bar.pack(fill="x")
# ── Callbacks ─────────────────────────────────────────────────────────────
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("<<ComboboxSelected>>", 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._refresh_saved()
def _load_games(self):
games = get_all_games(active_only=True)
@@ -146,6 +236,59 @@ class PredictorScreen(ttk.Frame):
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
@@ -183,7 +326,7 @@ class PredictorScreen(ttk.Frame):
def _display(self, tickets):
self._tree.delete(*self._tree.get_children())
game = get_game_by_name(self._game_var.get())
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):
@@ -201,6 +344,7 @@ class PredictorScreen(ttk.Frame):
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 _clear(self):
@@ -220,8 +364,7 @@ class PredictorScreen(ttk.Frame):
if not fp:
return
try:
game_id = self._game_id
export_predictions_excel(fp, game_id=game_id)
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))
@@ -237,8 +380,32 @@ class PredictorScreen(ttk.Frame):
if not fp:
return
try:
game_id = self._game_id
export_predictions_csv(fp, game_id=game_id)
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()