05/23 Phase 20
This commit is contained in:
@@ -16,7 +16,8 @@
|
|||||||
"Bash(python -m pytest tests/ -q)",
|
"Bash(python -m pytest tests/ -q)",
|
||||||
"Bash(python -c ' *)",
|
"Bash(python -c ' *)",
|
||||||
"Bash(python -m pytest tests/test_backup.py -v)",
|
"Bash(python -m pytest tests/test_backup.py -v)",
|
||||||
"Bash(python -m pytest --tb=short -q)"
|
"Bash(python -m pytest --tb=short -q)",
|
||||||
|
"Bash(python -m pytest tests/test_wheeling.py -v)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,6 +341,22 @@ All actions are logged to console and optionally to a log file:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### ✅ Phase 20 — Number Wheeling System
|
||||||
|
- [x] Write `core/wheeling.py`
|
||||||
|
- [x] `wheel_count(numbers, k) -> int` — C(n, k) preview, deduplicates input
|
||||||
|
- [x] `wheel_full(numbers, k) -> list[list[int]]` — all combinations, each sorted ascending
|
||||||
|
- [x] Raises `ValueError` for invalid k, k > pool size, or count > `MAX_TICKETS` (200)
|
||||||
|
- [x] Update `ui/predictor_ui.py`
|
||||||
|
- [x] Add "Wheel" as 4th tab in ttk.Notebook
|
||||||
|
- [x] Game dropdown (synced on refresh), Numbers entry, Pick spinbox (defaults to game's main_count)
|
||||||
|
- [x] Live preview label: "Will generate X tickets (C(n,k))" updates as user types
|
||||||
|
- [x] Range validation against game's main_max before generating
|
||||||
|
- [x] Results treeview (#, Numbers) + BallsBar detail strip on row select
|
||||||
|
- [x] Save to DB (strategy="Wheel") + Copy + Clear buttons
|
||||||
|
- [x] Write `tests/test_wheeling.py` — 18 tests (379/379 total passing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### ✅ Phase 19 — Dashboard Overdue Alert + DB Backup
|
### ✅ Phase 19 — Dashboard Overdue Alert + DB Backup
|
||||||
- [x] Add `backup_db(dest_dir=None) -> str` to `db/database.py` — copies live DB to timestamped file, creates dest dir if needed
|
- [x] Add `backup_db(dest_dir=None) -> str` to `db/database.py` — copies live DB to timestamped file, creates dest dir if needed
|
||||||
- [x] Add `restore_db(source_path: str) -> None` to `db/database.py` — overwrites live DB with backup file, raises FileNotFoundError if missing
|
- [x] Add `restore_db(source_path: str) -> None` to `db/database.py` — overwrites live DB with backup file, raises FileNotFoundError if missing
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
core/wheeling.py
|
||||||
|
----------------
|
||||||
|
Full-cover number wheeling.
|
||||||
|
|
||||||
|
wheel_count(numbers, k) — C(n, k) ticket count preview
|
||||||
|
wheel_full(numbers, k) — all C(n, k) sorted combinations
|
||||||
|
|
||||||
|
Raises ValueError for invalid inputs or if result exceeds MAX_TICKETS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from itertools import combinations
|
||||||
|
from math import comb
|
||||||
|
|
||||||
|
MAX_TICKETS = 200
|
||||||
|
|
||||||
|
|
||||||
|
def wheel_count(numbers: list | set, k: int) -> int:
|
||||||
|
"""Return how many tickets a full wheel would produce (C(n, k))."""
|
||||||
|
n = len(set(numbers))
|
||||||
|
if k < 1 or k > n:
|
||||||
|
return 0
|
||||||
|
return comb(n, k)
|
||||||
|
|
||||||
|
|
||||||
|
def wheel_full(numbers: list | set, k: int) -> list[list[int]]:
|
||||||
|
"""
|
||||||
|
Generate all C(n, k) combinations from *numbers*, each sorted ascending.
|
||||||
|
Duplicates in input are removed before wheeling.
|
||||||
|
Raises ValueError if k is out of range or count > MAX_TICKETS.
|
||||||
|
"""
|
||||||
|
pool = sorted(set(numbers))
|
||||||
|
n = len(pool)
|
||||||
|
|
||||||
|
if k < 1:
|
||||||
|
raise ValueError("Pick count must be at least 1.")
|
||||||
|
if k > n:
|
||||||
|
raise ValueError(
|
||||||
|
f"Pick count ({k}) exceeds the number pool size ({n})."
|
||||||
|
)
|
||||||
|
|
||||||
|
count = comb(n, k)
|
||||||
|
if count > MAX_TICKETS:
|
||||||
|
raise ValueError(
|
||||||
|
f"Wheel would produce {count:,} tickets (max {MAX_TICKETS:,}). "
|
||||||
|
f"Reduce your pool or pick count."
|
||||||
|
)
|
||||||
|
|
||||||
|
return [sorted(combo) for combo in combinations(pool, k)]
|
||||||
Binary file not shown.
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
tests/test_wheeling.py
|
||||||
|
-----------------------
|
||||||
|
Tests for core/wheeling.py — full-cover wheel generation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from core.wheeling import wheel_full, wheel_count, MAX_TICKETS
|
||||||
|
|
||||||
|
|
||||||
|
# ── wheel_count ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_wheel_count_basic():
|
||||||
|
assert wheel_count([1, 2, 3, 4, 5, 6], 5) == 6 # C(6,5)
|
||||||
|
|
||||||
|
def test_wheel_count_exact_pick():
|
||||||
|
assert wheel_count([1, 2, 3, 4, 5], 5) == 1 # C(5,5)
|
||||||
|
|
||||||
|
def test_wheel_count_larger():
|
||||||
|
assert wheel_count(list(range(1, 10)), 5) == 126 # C(9,5)
|
||||||
|
|
||||||
|
def test_wheel_count_k_zero_returns_zero():
|
||||||
|
assert wheel_count([1, 2, 3, 4, 5], 0) == 0
|
||||||
|
|
||||||
|
def test_wheel_count_k_exceeds_n_returns_zero():
|
||||||
|
assert wheel_count([1, 2, 3], 5) == 0
|
||||||
|
|
||||||
|
def test_wheel_count_deduplicates_input():
|
||||||
|
assert wheel_count([1, 1, 2, 3, 4, 5], 5) == 1 # C(5,5) after dedup
|
||||||
|
|
||||||
|
|
||||||
|
# ── wheel_full — valid cases ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_wheel_full_ticket_count():
|
||||||
|
tickets = wheel_full([1, 2, 3, 4, 5, 6], 5)
|
||||||
|
assert len(tickets) == 6
|
||||||
|
|
||||||
|
def test_wheel_full_single_ticket():
|
||||||
|
tickets = wheel_full([5, 14, 22, 36, 69], 5)
|
||||||
|
assert len(tickets) == 1
|
||||||
|
assert tickets[0] == [5, 14, 22, 36, 69]
|
||||||
|
|
||||||
|
def test_wheel_full_each_ticket_sorted():
|
||||||
|
tickets = wheel_full([10, 3, 7, 1, 5, 2], 4)
|
||||||
|
for t in tickets:
|
||||||
|
assert t == sorted(t)
|
||||||
|
|
||||||
|
def test_wheel_full_no_duplicate_tickets():
|
||||||
|
tickets = wheel_full(list(range(1, 9)), 5) # C(8,5) = 56
|
||||||
|
as_tuples = [tuple(t) for t in tickets]
|
||||||
|
assert len(as_tuples) == len(set(as_tuples))
|
||||||
|
|
||||||
|
def test_wheel_full_all_numbers_in_pool():
|
||||||
|
pool = [5, 14, 22, 36, 55, 69]
|
||||||
|
tickets = wheel_full(pool, 4)
|
||||||
|
for t in tickets:
|
||||||
|
for n in t:
|
||||||
|
assert n in pool
|
||||||
|
|
||||||
|
def test_wheel_full_deduplicates_input():
|
||||||
|
# [1,1,2,3,4,5] → pool [1,2,3,4,5] → C(5,5) = 1
|
||||||
|
tickets = wheel_full([1, 1, 2, 3, 4, 5], 5)
|
||||||
|
assert len(tickets) == 1
|
||||||
|
|
||||||
|
def test_wheel_full_at_cap(monkeypatch):
|
||||||
|
import core.wheeling as wm
|
||||||
|
monkeypatch.setattr(wm, "MAX_TICKETS", 56)
|
||||||
|
# C(8,5) = 56 exactly at cap — should pass
|
||||||
|
tickets = wm.wheel_full(list(range(1, 9)), 5)
|
||||||
|
assert len(tickets) == 56
|
||||||
|
|
||||||
|
def test_wheel_full_returns_list_of_lists():
|
||||||
|
tickets = wheel_full([1, 2, 3, 4, 5, 6], 5)
|
||||||
|
assert isinstance(tickets, list)
|
||||||
|
for t in tickets:
|
||||||
|
assert isinstance(t, list)
|
||||||
|
|
||||||
|
|
||||||
|
# ── wheel_full — error cases ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_wheel_full_k_zero_raises():
|
||||||
|
with pytest.raises(ValueError, match="at least 1"):
|
||||||
|
wheel_full([1, 2, 3, 4, 5], 0)
|
||||||
|
|
||||||
|
def test_wheel_full_k_exceeds_n_raises():
|
||||||
|
with pytest.raises(ValueError, match="exceeds"):
|
||||||
|
wheel_full([1, 2, 3], 5)
|
||||||
|
|
||||||
|
def test_wheel_full_exceeds_cap_raises():
|
||||||
|
# C(10,5) = 252 > MAX_TICKETS (200)
|
||||||
|
with pytest.raises(ValueError, match="252"):
|
||||||
|
wheel_full(list(range(1, 11)), 5)
|
||||||
|
|
||||||
|
def test_wheel_full_error_message_includes_count():
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
wheel_full(list(range(1, 11)), 5)
|
||||||
|
assert "252" in str(exc_info.value)
|
||||||
|
assert str(MAX_TICKETS) in str(exc_info.value)
|
||||||
+258
-4
@@ -1,11 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
ui/predictor_ui.py
|
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
|
Generate tab — pick strategy + game + count → generate tickets → save to DB
|
||||||
Saved tab — browse all saved predictions, see match count vs last real
|
Saved tab — browse all saved predictions, see match count vs last draw
|
||||||
draw, delete individual rows or clear all
|
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
|
import os
|
||||||
@@ -22,6 +23,7 @@ from core.predictor import (
|
|||||||
hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, quick_pick,
|
hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, quick_pick,
|
||||||
)
|
)
|
||||||
from core.checker import check_ticket, parse_numbers
|
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 core.exporter import export_predictions_excel, export_predictions_csv, ensure_exports_dir
|
||||||
from ui.widgets import BallsBar
|
from ui.widgets import BallsBar
|
||||||
|
|
||||||
@@ -48,6 +50,17 @@ _DESCRIPTIONS = {
|
|||||||
_TICKET_COUNTS = [str(n) for n in range(1, 11)]
|
_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:
|
def _count_matches(pred_numbers: str, last_draw) -> str:
|
||||||
"""Compare prediction numbers to last draw, return 'X/5' string."""
|
"""Compare prediction numbers to last draw, return 'X/5' string."""
|
||||||
if last_draw is None:
|
if last_draw is None:
|
||||||
@@ -67,6 +80,7 @@ class PredictorScreen(ttk.Frame):
|
|||||||
self._game_id: int | None = None
|
self._game_id: int | None = None
|
||||||
self._tickets: list[dict] = []
|
self._tickets: list[dict] = []
|
||||||
self._strategy_name: str = "Hot Numbers"
|
self._strategy_name: str = "Hot Numbers"
|
||||||
|
self._wheel_tickets: list[list[int]] = []
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
# ── UI construction ───────────────────────────────────────────────────────
|
# ── UI construction ───────────────────────────────────────────────────────
|
||||||
@@ -78,13 +92,16 @@ class PredictorScreen(ttk.Frame):
|
|||||||
gen_frame = ttk.Frame(nb)
|
gen_frame = ttk.Frame(nb)
|
||||||
saved_frame = ttk.Frame(nb)
|
saved_frame = ttk.Frame(nb)
|
||||||
check_frame = ttk.Frame(nb)
|
check_frame = ttk.Frame(nb)
|
||||||
|
wheel_frame = ttk.Frame(nb)
|
||||||
nb.add(gen_frame, text="Generate")
|
nb.add(gen_frame, text="Generate")
|
||||||
nb.add(saved_frame, text="Saved")
|
nb.add(saved_frame, text="Saved")
|
||||||
nb.add(check_frame, text="Check Ticket")
|
nb.add(check_frame, text="Check Ticket")
|
||||||
|
nb.add(wheel_frame, text="Wheel")
|
||||||
|
|
||||||
self._build_generate_tab(gen_frame)
|
self._build_generate_tab(gen_frame)
|
||||||
self._build_saved_tab(saved_frame)
|
self._build_saved_tab(saved_frame)
|
||||||
self._build_check_tab(check_frame)
|
self._build_check_tab(check_frame)
|
||||||
|
self._build_wheel_tab(wheel_frame)
|
||||||
|
|
||||||
# ── Generate tab ──────────────────────────────────────────────────────────
|
# ── Generate tab ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -248,6 +265,7 @@ class PredictorScreen(ttk.Frame):
|
|||||||
def refresh(self):
|
def refresh(self):
|
||||||
self._load_games()
|
self._load_games()
|
||||||
self._load_check_games()
|
self._load_check_games()
|
||||||
|
self._load_wheel_games()
|
||||||
self._refresh_saved()
|
self._refresh_saved()
|
||||||
|
|
||||||
def _load_games(self):
|
def _load_games(self):
|
||||||
@@ -705,6 +723,242 @@ class PredictorScreen(ttk.Frame):
|
|||||||
w.destroy()
|
w.destroy()
|
||||||
self._chk_summary_var.set("")
|
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):
|
def _load_check_games(self):
|
||||||
games = get_all_games(active_only=True)
|
games = get_all_games(active_only=True)
|
||||||
names = [g["name"] for g in games]
|
names = [g["name"] for g in games]
|
||||||
|
|||||||
Reference in New Issue
Block a user