715 lines
29 KiB
Python
715 lines
29 KiB
Python
"""
|
||
ui/predictor_ui.py
|
||
------------------
|
||
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
|
||
import tkinter as tk
|
||
from tkinter import ttk, filedialog, messagebox
|
||
import logging
|
||
|
||
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, quick_pick,
|
||
)
|
||
from core.checker import check_ticket, parse_numbers
|
||
from core.exporter import export_predictions_excel, export_predictions_csv, ensure_exports_dir
|
||
from ui.widgets import BallsBar
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_STRATEGIES = {
|
||
"Hot Numbers": hot_numbers,
|
||
"Due Numbers": due_numbers,
|
||
"Weighted Random": weighted_random,
|
||
"Monte Carlo": monte_carlo,
|
||
"Positional": positional_pick,
|
||
"Quick Pick": quick_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).",
|
||
"Quick Pick": "Pure random selection — no historical data required.",
|
||
}
|
||
|
||
_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] = []
|
||
self._strategy_name: str = "Hot Numbers"
|
||
self._build_ui()
|
||
|
||
# ── UI construction ───────────────────────────────────────────────────────
|
||
|
||
def _build_ui(self):
|
||
nb = ttk.Notebook(self)
|
||
nb.pack(fill="both", expand=True)
|
||
|
||
gen_frame = ttk.Frame(nb)
|
||
saved_frame = ttk.Frame(nb)
|
||
check_frame = ttk.Frame(nb)
|
||
nb.add(gen_frame, text="Generate")
|
||
nb.add(saved_frame, text="Saved")
|
||
nb.add(check_frame, text="Check Ticket")
|
||
|
||
self._build_generate_tab(gen_frame)
|
||
self._build_saved_tab(saved_frame)
|
||
self._build_check_tab(check_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")
|
||
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))
|
||
|
||
ttk.Label(bar, text="Exclude:", padding=(12, 0, 0, 0)).pack(side="left")
|
||
self._exclude_var = tk.StringVar()
|
||
ttk.Entry(bar, textvariable=self._exclude_var, width=14).pack(side="left", padx=(4, 0))
|
||
|
||
self._gen_btn = ttk.Button(bar, text="Generate", command=self._generate)
|
||
self._gen_btn.pack(side="left", padx=(12, 0))
|
||
|
||
# Results treeview
|
||
tree_frame = ttk.Frame(parent)
|
||
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")
|
||
|
||
self._tree.bind("<<TreeviewSelect>>", self._on_gen_row_select)
|
||
|
||
# ── Ball detail strip ─────────────────────────────────────────────────
|
||
ttk.Separator(parent, orient="horizontal").pack(fill="x", padx=6)
|
||
self._gen_detail = ttk.Frame(parent, padding=(8, 3))
|
||
self._gen_detail.pack(fill="x")
|
||
|
||
# Bottom bar
|
||
bottom = ttk.Frame(parent, 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")
|
||
|
||
self._copy_btn = ttk.Button(
|
||
bottom, text="Copy", command=self._copy_tickets, state="disabled"
|
||
)
|
||
self._copy_btn.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))
|
||
|
||
# ── Saved tab ─────────────────────────────────────────────────────────────
|
||
|
||
def _build_saved_tab(self, parent):
|
||
# Filter + action bar
|
||
bar = ttk.Frame(parent, padding=(6, 6, 6, 4))
|
||
bar.pack(fill="x")
|
||
|
||
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._load_check_games()
|
||
self._refresh_saved()
|
||
|
||
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
|
||
|
||
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
|
||
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:
|
||
excl = self._parse_exclude()
|
||
tickets = [strategy_fn(self._game_id, exclude=excl) for _ in range(count)]
|
||
self._tickets = tickets
|
||
self._display(tickets)
|
||
self._save_btn.config(state="normal")
|
||
self._copy_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")
|
||
self._refresh_saved()
|
||
logger.info("[PREDICT] Saved %d prediction(s) to DB", saved)
|
||
|
||
def _parse_exclude(self) -> set:
|
||
raw = self._exclude_var.get().strip()
|
||
if not raw:
|
||
return set()
|
||
result = set()
|
||
for tok in raw.replace(",", " ").split():
|
||
try:
|
||
result.add(int(tok))
|
||
except ValueError:
|
||
pass
|
||
return result
|
||
|
||
def _copy_tickets(self):
|
||
if not self._tickets:
|
||
return
|
||
game = get_game_by_name(self._game_var.get())
|
||
show_bonus = game is not None and game["bonus_count"] > 0
|
||
lines = []
|
||
for i, t in enumerate(self._tickets, 1):
|
||
nums = " ".join(f"{n:02d}" for n in t["numbers"])
|
||
line = f"Ticket {i}: {nums}"
|
||
if show_bonus and t["bonus"] is not None:
|
||
line += f" + {t['bonus']:02d}"
|
||
lines.append(line)
|
||
self.clipboard_clear()
|
||
self.clipboard_append("\n".join(lines))
|
||
self._status_var.set("Copied to clipboard.")
|
||
|
||
def _on_gen_row_select(self, _event=None):
|
||
for w in self._gen_detail.winfo_children():
|
||
w.destroy()
|
||
sel = self._tree.selection()
|
||
if not sel:
|
||
return
|
||
vals = self._tree.item(sel[0], "values")
|
||
# vals: (#, numbers_fmt, bonus)
|
||
try:
|
||
nums = [int(x) for x in str(vals[1]).split() if x.isdigit()]
|
||
b_txt = str(vals[2])
|
||
bonus = int(b_txt) if b_txt.isdigit() else None
|
||
except Exception:
|
||
return
|
||
if nums:
|
||
BallsBar(self._gen_detail, numbers=nums, bonus=bonus).pack(anchor="w")
|
||
|
||
def _clear(self):
|
||
self._tickets = []
|
||
self._tree.delete(*self._tree.get_children())
|
||
for w in self._gen_detail.winfo_children():
|
||
w.destroy()
|
||
self._save_btn.config(state="disabled")
|
||
self._copy_btn.config(state="disabled")
|
||
self._status_var.set("")
|
||
|
||
def _export_excel(self):
|
||
ensure_exports_dir()
|
||
fp = filedialog.asksaveasfilename(
|
||
title="Export predictions",
|
||
defaultextension=".xlsx",
|
||
filetypes=[("Excel workbook", "*.xlsx")],
|
||
initialfile=f"predictions_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.xlsx",
|
||
)
|
||
if not fp:
|
||
return
|
||
try:
|
||
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))
|
||
|
||
def _export_csv(self):
|
||
ensure_exports_dir()
|
||
fp = filedialog.asksaveasfilename(
|
||
title="Export predictions",
|
||
defaultextension=".csv",
|
||
filetypes=[("CSV file", "*.csv")],
|
||
initialfile=f"predictions_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.csv",
|
||
)
|
||
if not fp:
|
||
return
|
||
try:
|
||
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()
|
||
|
||
# ── Check Ticket tab ──────────────────────────────────────────────────────
|
||
|
||
def _build_check_tab(self, parent):
|
||
# Input bar
|
||
bar = ttk.Frame(parent, padding=(6, 8, 6, 4))
|
||
bar.pack(fill="x")
|
||
|
||
ttk.Label(bar, text="Game:").pack(side="left")
|
||
self._chk_game_var = tk.StringVar()
|
||
self._chk_game_cb = ttk.Combobox(
|
||
bar, textvariable=self._chk_game_var, state="readonly", width=15
|
||
)
|
||
self._chk_game_cb.pack(side="left", padx=(4, 14))
|
||
|
||
ttk.Label(bar, text="Numbers:").pack(side="left")
|
||
self._chk_nums_var = tk.StringVar()
|
||
chk_nums_entry = ttk.Entry(bar, textvariable=self._chk_nums_var, width=22)
|
||
chk_nums_entry.pack(side="left", padx=(4, 4))
|
||
chk_nums_entry.bind("<Return>", lambda e: self._run_check())
|
||
|
||
ttk.Label(bar, text="Bonus:").pack(side="left")
|
||
self._chk_bonus_var = tk.StringVar()
|
||
chk_bonus_entry = ttk.Entry(bar, textvariable=self._chk_bonus_var, width=5)
|
||
chk_bonus_entry.pack(side="left", padx=(4, 14))
|
||
chk_bonus_entry.bind("<Return>", lambda e: self._run_check())
|
||
|
||
ttk.Button(bar, text="Check", command=self._run_check).pack(side="left", padx=(0, 4))
|
||
ttk.Button(bar, text="Clear", command=self._clear_check).pack(side="left")
|
||
|
||
self._chk_status_var = tk.StringVar()
|
||
ttk.Label(bar, textvariable=self._chk_status_var,
|
||
foreground="#c0392b").pack(side="left", padx=(12, 0))
|
||
|
||
# Hint text below bar
|
||
hint = ttk.Frame(parent, padding=(6, 0, 6, 4))
|
||
hint.pack(fill="x")
|
||
ttk.Label(hint,
|
||
text="Enter space- or comma-separated main numbers, and optionally a bonus number.",
|
||
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))
|
||
|
||
c_cols = ("date", "draw_numbers", "bonus", "main", "bonus_hit", "tier")
|
||
self._chk_tree = ttk.Treeview(
|
||
tree_frame, columns=c_cols, show="headings", selectmode="browse"
|
||
)
|
||
self._chk_tree.heading("date", text="Draw Date")
|
||
self._chk_tree.heading("draw_numbers", text="Draw Numbers")
|
||
self._chk_tree.heading("bonus", text="Bonus")
|
||
self._chk_tree.heading("main", text="Main Hits")
|
||
self._chk_tree.heading("bonus_hit", text="Bonus Hit")
|
||
self._chk_tree.heading("tier", text="Prize Tier")
|
||
|
||
self._chk_tree.column("date", width=100, anchor="center", stretch=False)
|
||
self._chk_tree.column("draw_numbers", width=190, anchor="w")
|
||
self._chk_tree.column("bonus", width=55, anchor="center", stretch=False)
|
||
self._chk_tree.column("main", width=70, anchor="center", stretch=False)
|
||
self._chk_tree.column("bonus_hit", width=70, anchor="center", stretch=False)
|
||
self._chk_tree.column("tier", width=160, anchor="w", stretch=False)
|
||
|
||
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self._chk_tree.yview)
|
||
hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self._chk_tree.xview)
|
||
self._chk_tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
|
||
self._chk_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)
|
||
|
||
# Row colour tags
|
||
self._chk_tree.tag_configure("jackpot", foreground="#8e44ad", font=("TkDefaultFont", 9, "bold"))
|
||
self._chk_tree.tag_configure("high", foreground="#1e8449")
|
||
self._chk_tree.tag_configure("low", foreground="#555555")
|
||
|
||
self._chk_tree.bind("<<TreeviewSelect>>", self._on_chk_row_select)
|
||
|
||
# ── Ball detail strip ─────────────────────────────────────────────────
|
||
ttk.Separator(parent, orient="horizontal").pack(fill="x", padx=6)
|
||
self._chk_detail = ttk.Frame(parent, padding=(8, 3))
|
||
self._chk_detail.pack(fill="x")
|
||
|
||
# Summary label
|
||
self._chk_summary_var = tk.StringVar()
|
||
ttk.Label(parent, textvariable=self._chk_summary_var,
|
||
anchor="e", padding=(6, 2)).pack(fill="x")
|
||
|
||
def _run_check(self):
|
||
self._chk_status_var.set("")
|
||
self._chk_tree.delete(*self._chk_tree.get_children())
|
||
self._chk_summary_var.set("")
|
||
|
||
# Resolve game
|
||
game = get_game_by_name(self._chk_game_var.get())
|
||
if game is None:
|
||
self._chk_status_var.set("Select a game first.")
|
||
return
|
||
|
||
# Parse main numbers
|
||
try:
|
||
numbers = parse_numbers(self._chk_nums_var.get())
|
||
except ValueError as e:
|
||
self._chk_status_var.set(str(e))
|
||
return
|
||
|
||
if len(numbers) != game["main_count"]:
|
||
self._chk_status_var.set(
|
||
f"{game['name']} requires {game['main_count']} main numbers "
|
||
f"(got {len(numbers)})."
|
||
)
|
||
return
|
||
|
||
# Validate range
|
||
invalid = [n for n in numbers if not (1 <= n <= game["main_max"])]
|
||
if invalid:
|
||
self._chk_status_var.set(
|
||
f"Numbers out of range 1–{game['main_max']}: {invalid}"
|
||
)
|
||
return
|
||
|
||
# Parse optional bonus
|
||
bonus = None
|
||
raw_bonus = self._chk_bonus_var.get().strip()
|
||
if raw_bonus:
|
||
if not raw_bonus.isdigit():
|
||
self._chk_status_var.set("Bonus must be a number.")
|
||
return
|
||
bonus = int(raw_bonus)
|
||
if game["bonus_max"] and not (1 <= bonus <= game["bonus_max"]):
|
||
self._chk_status_var.set(
|
||
f"Bonus out of range 1–{game['bonus_max']}."
|
||
)
|
||
return
|
||
|
||
results = check_ticket(game["id"], numbers, bonus=bonus)
|
||
|
||
for r in results:
|
||
nums_fmt = " ".join(f"{n:02d}" for n in r["draw_numbers"])
|
||
bonus_str = str(r["draw_bonus"]) if r["draw_bonus"] is not None else "—"
|
||
bonus_hit = "✓" if r["bonus_match"] else "—"
|
||
tier = r["prize_tier"]
|
||
|
||
if tier == "Jackpot":
|
||
tag = "jackpot"
|
||
elif r["main_matches"] >= 3 or r["bonus_match"]:
|
||
tag = "high"
|
||
else:
|
||
tag = "low"
|
||
|
||
self._chk_tree.insert("", "end", values=(
|
||
r["draw_date"], nums_fmt, bonus_str,
|
||
r["main_matches"], bonus_hit, tier,
|
||
), tags=(tag,))
|
||
|
||
total = len(results)
|
||
best = results[0]["prize_tier"] if results else "—"
|
||
self._chk_summary_var.set(
|
||
f"{total} draw{'s' if total != 1 else ''} matched • Best: {best}"
|
||
)
|
||
|
||
def _on_chk_row_select(self, _event=None):
|
||
for w in self._chk_detail.winfo_children():
|
||
w.destroy()
|
||
sel = self._chk_tree.selection()
|
||
if not sel:
|
||
return
|
||
vals = self._chk_tree.item(sel[0], "values")
|
||
# vals: (date, draw_numbers_fmt, bonus, main_hits, bonus_hit, tier)
|
||
try:
|
||
draw_nums = [int(x) for x in str(vals[1]).split() if x.isdigit()]
|
||
db_txt = str(vals[2])
|
||
draw_bonus = int(db_txt) if db_txt.isdigit() else None
|
||
except Exception:
|
||
return
|
||
if not draw_nums:
|
||
return
|
||
|
||
# Ticket numbers from the input entries
|
||
try:
|
||
ticket_nums = parse_numbers(self._chk_nums_var.get())
|
||
except Exception:
|
||
ticket_nums = []
|
||
raw_b = self._chk_bonus_var.get().strip()
|
||
ticket_bonus = int(raw_b) if raw_b.isdigit() else None
|
||
|
||
ticket_set = set(ticket_nums)
|
||
draw_set = set(draw_nums)
|
||
_GREEN = ("#27ae60", "#ffffff")
|
||
_GREY = ("#cccccc", "#888888")
|
||
|
||
ticket_hi = {n: (_GREEN if n in draw_set else _GREY) for n in ticket_nums}
|
||
draw_hi = {n: _GREEN for n in draw_nums if n in ticket_set}
|
||
if ticket_bonus is not None:
|
||
ticket_hi[ticket_bonus] = (
|
||
_GREEN if draw_bonus is not None and ticket_bonus == draw_bonus else _GREY
|
||
)
|
||
if draw_bonus is not None and ticket_bonus is not None and draw_bonus == ticket_bonus:
|
||
draw_hi[draw_bonus] = _GREEN
|
||
|
||
grid = ttk.Frame(self._chk_detail)
|
||
grid.pack(anchor="w")
|
||
if ticket_nums:
|
||
ttk.Label(grid, text="Ticket:", foreground="#555555",
|
||
font=("TkDefaultFont", 8), width=7).grid(row=0, column=0, sticky="w")
|
||
BallsBar(grid, numbers=ticket_nums, bonus=ticket_bonus,
|
||
highlights=ticket_hi).grid(row=0, column=1, sticky="w")
|
||
ttk.Label(grid, text="Draw:", foreground="#555555",
|
||
font=("TkDefaultFont", 8), width=7).grid(row=1, column=0, sticky="w")
|
||
BallsBar(grid, numbers=draw_nums, bonus=draw_bonus,
|
||
highlights=draw_hi).grid(row=1, column=1, sticky="w")
|
||
|
||
def _clear_check(self):
|
||
self._chk_nums_var.set("")
|
||
self._chk_bonus_var.set("")
|
||
self._chk_status_var.set("")
|
||
self._chk_tree.delete(*self._chk_tree.get_children())
|
||
for w in self._chk_detail.winfo_children():
|
||
w.destroy()
|
||
self._chk_summary_var.set("")
|
||
|
||
def _load_check_games(self):
|
||
games = get_all_games(active_only=True)
|
||
names = [g["name"] for g in games]
|
||
self._chk_game_cb["values"] = names
|
||
if not self._chk_game_var.get() or self._chk_game_var.get() not in names:
|
||
if names:
|
||
self._chk_game_var.set(names[0])
|