05/23 Phase 20

This commit is contained in:
2026-05-23 16:38:20 -04:00
parent 2553406254
commit ea30c7d838
6 changed files with 423 additions and 5 deletions
+258 -4
View File
@@ -1,11 +1,12 @@
"""
ui/predictor_ui.py
------------------
Prediction generator screen — two tabs inside a ttk.Notebook.
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 real
draw, delete individual rows or clear all
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
@@ -22,6 +23,7 @@ from core.predictor import (
hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, 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
@@ -48,6 +50,17 @@ _DESCRIPTIONS = {
_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:
@@ -67,6 +80,7 @@ class PredictorScreen(ttk.Frame):
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 ───────────────────────────────────────────────────────
@@ -78,13 +92,16 @@ class PredictorScreen(ttk.Frame):
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 ──────────────────────────────────────────────────────────
@@ -248,6 +265,7 @@ class PredictorScreen(ttk.Frame):
def refresh(self):
self._load_games()
self._load_check_games()
self._load_wheel_games()
self._refresh_saved()
def _load_games(self):
@@ -705,6 +723,242 @@ class PredictorScreen(ttk.Frame):
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("<<ComboboxSelected>>", 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("<Return>", 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("<<TreeviewSelect>>", 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]