05/23 Phase 20
This commit is contained in:
+258
-4
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user