05/23 Phase 16
This commit is contained in:
@@ -341,6 +341,19 @@ All actions are logged to console and optionally to a log file:
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 16 — Lottery Ball Display
|
||||
- [x] Write `ui/widgets.py`
|
||||
- [x] `ball_color(number, is_bonus) -> (bg, fg)` — range-based colour lookup (pure, no Tk)
|
||||
- [x] `BallsBar(tk.Canvas)` — draws numbered balls; supports `highlights` dict for per-ball colour override; inherits parent background
|
||||
- [x] Update `ui/dashboard.py` — last-draw cards use `BallsBar` for numbers; hot-numbers section uses small-radius (`r=11`) balls
|
||||
- [x] Update `ui/history.py` — ball detail strip below treeview; shows `BallsBar` for selected row; clears on filter/populate
|
||||
- [x] Update `ui/predictor_ui.py`
|
||||
- [x] Generate tab: detail strip shows selected ticket as balls
|
||||
- [x] Check Ticket tab: detail strip shows ticket (green=matched, grey=unmatched) and draw numbers (green=matched) side by side
|
||||
- [x] Write `tests/test_widgets.py` — 12 tests (308/308 total passing)
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 15 — Odds Calculator
|
||||
- [x] Write `core/odds.py`
|
||||
- [x] `total_combinations(main_count, main_max, bonus_count, bonus_max) -> int`
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
tests/test_widgets.py
|
||||
---------------------
|
||||
Tests for ui/widgets.py — ball_color() is pure and testable without a display.
|
||||
BallsBar creation tests require Tkinter and are skipped when unavailable.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from ui.widgets import ball_color, BallsBar
|
||||
|
||||
|
||||
def _has_display():
|
||||
try:
|
||||
import tkinter as tk
|
||||
r = tk.Tk(); r.withdraw(); r.destroy()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ── ball_color — pure function, no display needed ─────────────────────────────
|
||||
|
||||
def test_ball_color_returns_two_hex_strings():
|
||||
bg, fg = ball_color(7)
|
||||
assert bg.startswith("#") and len(bg) == 7
|
||||
assert fg.startswith("#") and len(fg) == 7
|
||||
|
||||
def test_ball_color_bonus_is_red():
|
||||
bg, _ = ball_color(7, is_bonus=True)
|
||||
assert bg == "#e74c3c"
|
||||
|
||||
def test_ball_color_bonus_same_regardless_of_number():
|
||||
assert ball_color(1, is_bonus=True) == ball_color(26, is_bonus=True)
|
||||
assert ball_color(99, is_bonus=True) == ball_color(5, is_bonus=True)
|
||||
|
||||
def test_ball_color_different_ranges_have_different_colors():
|
||||
bgs = {ball_color(n)[0] for n in [1, 15, 25, 35, 45, 55, 65, 75]}
|
||||
assert len(bgs) >= 5 # at least 5 distinct background colors
|
||||
|
||||
def test_ball_color_boundary_values():
|
||||
# Each range boundary should resolve without error
|
||||
for n in [1, 9, 10, 19, 20, 29, 30, 39, 40, 49, 50, 59, 60, 69, 70, 99]:
|
||||
bg, fg = ball_color(n)
|
||||
assert bg.startswith("#")
|
||||
assert fg.startswith("#")
|
||||
|
||||
def test_ball_color_out_of_defined_range():
|
||||
# Number > 99 falls back gracefully
|
||||
bg, fg = ball_color(200)
|
||||
assert bg.startswith("#")
|
||||
assert fg.startswith("#")
|
||||
|
||||
def test_ball_color_is_deterministic():
|
||||
for n in [1, 7, 14, 22, 35, 49, 69]:
|
||||
assert ball_color(n) == ball_color(n)
|
||||
|
||||
def test_ball_color_non_bonus_not_red():
|
||||
# Main balls in defined ranges should not be the bonus red
|
||||
for n in range(1, 70):
|
||||
bg, _ = ball_color(n, is_bonus=False)
|
||||
assert bg != "#e74c3c" or n >= 60 # only 60-69 range is red
|
||||
|
||||
|
||||
# ── BallsBar — needs display ──────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.skipif(not _has_display(), reason="no display available")
|
||||
def test_ballsbar_creates_widget():
|
||||
import tkinter as tk
|
||||
try:
|
||||
root = tk.Tk(); root.withdraw()
|
||||
except Exception:
|
||||
pytest.skip("Tkinter init failed")
|
||||
try:
|
||||
bar = BallsBar(root, numbers=[1, 7, 14, 22, 35], bonus=3)
|
||||
assert bar.winfo_exists()
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _has_display(), reason="no display available")
|
||||
def test_ballsbar_no_bonus():
|
||||
import tkinter as tk
|
||||
try:
|
||||
root = tk.Tk(); root.withdraw()
|
||||
except Exception:
|
||||
pytest.skip("Tkinter init failed")
|
||||
try:
|
||||
bar = BallsBar(root, numbers=[5, 12, 30, 44, 66])
|
||||
assert bar.winfo_exists()
|
||||
assert int(bar.cget("width")) > 0
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _has_display(), reason="no display available")
|
||||
def test_ballsbar_with_highlights():
|
||||
import tkinter as tk
|
||||
try:
|
||||
root = tk.Tk(); root.withdraw()
|
||||
except Exception:
|
||||
pytest.skip("Tkinter init failed")
|
||||
try:
|
||||
hi = {7: ("#27ae60", "#ffffff"), 14: ("#cccccc", "#888888")}
|
||||
bar = BallsBar(root, numbers=[1, 7, 14, 22, 35], highlights=hi)
|
||||
assert bar.winfo_exists()
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _has_display(), reason="no display available")
|
||||
def test_ballsbar_larger_radius():
|
||||
import tkinter as tk
|
||||
try:
|
||||
root = tk.Tk(); root.withdraw()
|
||||
except Exception:
|
||||
pytest.skip("Tkinter init failed")
|
||||
try:
|
||||
bar_small = BallsBar(root, numbers=[1, 2, 3], radius=11)
|
||||
bar_normal = BallsBar(root, numbers=[1, 2, 3], radius=14)
|
||||
w_small = int(bar_small.cget("width"))
|
||||
w_normal = int(bar_normal.cget("width"))
|
||||
assert w_normal > w_small
|
||||
finally:
|
||||
root.destroy()
|
||||
+9
-13
@@ -16,6 +16,7 @@ from datetime import date, timedelta
|
||||
from db.database import get_db_stats
|
||||
from db.models import get_all_games, get_last_draw, get_draw_count, get_predictions
|
||||
from core.analyzer import frequency_analysis
|
||||
from ui.widgets import BallsBar
|
||||
|
||||
# Weekday indices: Monday=0 … Sunday=6
|
||||
_DRAW_DAYS: dict[str, list[int]] = {
|
||||
@@ -131,13 +132,10 @@ class DashboardScreen(ttk.Frame):
|
||||
else:
|
||||
ttk.Label(card, text=draw["draw_date"],
|
||||
foreground="#555555", font=_CARD_FONT).pack(anchor="w")
|
||||
ttk.Label(card,
|
||||
text=_fmt_numbers(draw["numbers"]),
|
||||
font=_NUMBER_FONT, foreground="#1a1a2e").pack(anchor="w", pady=(6, 2))
|
||||
if game["bonus_count"] > 0 and draw["bonus"]:
|
||||
bonus_label = "Bonus" if game["name"] != "Powerball" else "Powerball"
|
||||
ttk.Label(card, text=f"{bonus_label}: {draw['bonus']}",
|
||||
foreground="#2471a3", font=_CARD_FONT).pack(anchor="w")
|
||||
nums = [int(n) for n in draw["numbers"].split(",") if n.strip().isdigit()]
|
||||
bonus = (int(draw["bonus"]) if draw["bonus"] and str(draw["bonus"]).isdigit()
|
||||
and game["bonus_count"] > 0 else None)
|
||||
BallsBar(card, numbers=nums, bonus=bonus).pack(anchor="w", pady=(6, 2))
|
||||
if draw["multiplier"]:
|
||||
ttk.Label(card, text=f"Multiplier: {draw['multiplier']}",
|
||||
foreground="#777777", font=_CARD_FONT).pack(anchor="w")
|
||||
@@ -191,10 +189,8 @@ class DashboardScreen(ttk.Frame):
|
||||
font=_CARD_FONT).pack(side="left")
|
||||
continue
|
||||
|
||||
top5 = sorted(freq, key=freq.get, reverse=True)[:5]
|
||||
nums_str = " ".join(f"{n:02d}" for n in sorted(top5))
|
||||
ttk.Label(row, text=nums_str, font=_CARD_FONT,
|
||||
foreground="#1a5276").pack(side="left")
|
||||
counts_str = " ".join(f"({freq[n]}×)" for n in sorted(top5))
|
||||
ttk.Label(row, text=f" {counts_str}", foreground="#aaaaaa",
|
||||
top5 = sorted(sorted(freq, key=freq.get, reverse=True)[:5])
|
||||
BallsBar(row, numbers=top5, radius=11).pack(side="left", padx=(0, 6))
|
||||
counts_str = " ".join(f"({freq[n]}×)" for n in top5)
|
||||
ttk.Label(row, text=counts_str, foreground="#aaaaaa",
|
||||
font=_CARD_FONT).pack(side="left")
|
||||
|
||||
@@ -13,6 +13,7 @@ from db.models import get_all_games, get_game_by_name, get_draws_with_game
|
||||
from core.exporter import (
|
||||
export_draws_excel, export_draws_csv, default_path, ensure_exports_dir,
|
||||
)
|
||||
from ui.widgets import BallsBar
|
||||
|
||||
_COLUMNS = ("game", "date", "numbers", "bonus", "multiplier", "source")
|
||||
_LABELS = {
|
||||
@@ -115,6 +116,15 @@ class HistoryScreen(ttk.Frame):
|
||||
tree_frame.rowconfigure(0, weight=1)
|
||||
tree_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# ── Ball detail strip ─────────────────────────────────────────────────
|
||||
ttk.Separator(self, orient="horizontal").pack(fill="x", padx=6)
|
||||
self._detail_frame = ttk.Frame(self, padding=(8, 3, 8, 3))
|
||||
self._detail_frame.pack(fill="x")
|
||||
ttk.Label(self._detail_frame, text="Select a row to preview.",
|
||||
foreground="#aaaaaa", font=("TkDefaultFont", 8)).pack(anchor="w")
|
||||
|
||||
self._tree.bind("<<TreeviewSelect>>", self._on_row_select)
|
||||
|
||||
# ── Row count ─────────────────────────────────────────────────────────
|
||||
self._count_var = tk.StringVar(value="0 rows")
|
||||
ttk.Label(self, textvariable=self._count_var, anchor="e",
|
||||
@@ -173,6 +183,7 @@ class HistoryScreen(ttk.Frame):
|
||||
|
||||
def _populate(self, rows):
|
||||
self._tree.delete(*self._tree.get_children())
|
||||
self._clear_detail()
|
||||
for row in rows:
|
||||
self._tree.insert("", "end", values=(
|
||||
row["game_name"],
|
||||
@@ -185,6 +196,38 @@ class HistoryScreen(ttk.Frame):
|
||||
count = len(rows)
|
||||
self._count_var.set(f"{count} row{'s' if count != 1 else ''}")
|
||||
|
||||
def _clear_detail(self):
|
||||
for w in self._detail_frame.winfo_children():
|
||||
w.destroy()
|
||||
ttk.Label(self._detail_frame, text="Select a row to preview.",
|
||||
foreground="#aaaaaa", font=("TkDefaultFont", 8)).pack(anchor="w")
|
||||
|
||||
def _on_row_select(self, _event=None):
|
||||
sel = self._tree.selection()
|
||||
if not sel:
|
||||
self._clear_detail()
|
||||
return
|
||||
vals = self._tree.item(sel[0], "values")
|
||||
# vals: (game, date, numbers_fmt, bonus, multiplier, source)
|
||||
nums_text = vals[2] # e.g. "1 13 36 61 69"
|
||||
bonus_text = str(vals[3])
|
||||
try:
|
||||
nums = [int(x) for x in nums_text.split() if x.isdigit()]
|
||||
bonus = int(bonus_text) if bonus_text.isdigit() else None
|
||||
except Exception:
|
||||
self._clear_detail()
|
||||
return
|
||||
if not nums:
|
||||
self._clear_detail()
|
||||
return
|
||||
for w in self._detail_frame.winfo_children():
|
||||
w.destroy()
|
||||
row_frame = ttk.Frame(self._detail_frame)
|
||||
row_frame.pack(anchor="w")
|
||||
ttk.Label(row_frame, text=vals[1], foreground="#555555",
|
||||
font=("TkDefaultFont", 8), width=11).pack(side="left")
|
||||
BallsBar(row_frame, numbers=nums, bonus=bonus).pack(side="left")
|
||||
|
||||
# ── Sorting ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _sort_by(self, col):
|
||||
|
||||
@@ -23,6 +23,7 @@ from core.predictor import (
|
||||
)
|
||||
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__)
|
||||
|
||||
@@ -137,6 +138,13 @@ class PredictorScreen(ttk.Frame):
|
||||
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")
|
||||
@@ -352,9 +360,28 @@ class PredictorScreen(ttk.Frame):
|
||||
self._refresh_saved()
|
||||
logger.info("[PREDICT] Saved %d prediction(s) to DB", saved)
|
||||
|
||||
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._status_var.set("")
|
||||
|
||||
@@ -491,6 +518,13 @@ class PredictorScreen(ttk.Frame):
|
||||
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,
|
||||
@@ -569,11 +603,64 @@ class PredictorScreen(ttk.Frame):
|
||||
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):
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
ui/widgets.py
|
||||
-------------
|
||||
Shared reusable widgets.
|
||||
|
||||
ball_color(number, is_bonus) -> (bg_hex, fg_hex)
|
||||
BallsBar — tk.Canvas that draws a horizontal row of numbered lottery balls
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
|
||||
# (low, high, bg_hex, fg_hex) — range-based colour scheme
|
||||
_RANGES = [
|
||||
( 1, 9, "#f0f0f0", "#333333"),
|
||||
(10, 19, "#ffd700", "#333333"),
|
||||
(20, 29, "#ff7f50", "#ffffff"),
|
||||
(30, 39, "#4a9edd", "#ffffff"),
|
||||
(40, 49, "#3cb371", "#ffffff"),
|
||||
(50, 59, "#9b59b6", "#ffffff"),
|
||||
(60, 69, "#e74c3c", "#ffffff"),
|
||||
(70, 99, "#555555", "#ffffff"),
|
||||
]
|
||||
_BONUS_BG = "#e74c3c"
|
||||
_BONUS_FG = "#ffffff"
|
||||
_FALLBACK = ("#aaaaaa", "#333333")
|
||||
|
||||
|
||||
def ball_color(number: int, is_bonus: bool = False) -> tuple[str, str]:
|
||||
"""Return (bg_hex, fg_hex) for a lottery ball. Pure function, no Tk needed."""
|
||||
if is_bonus:
|
||||
return _BONUS_BG, _BONUS_FG
|
||||
for lo, hi, bg, fg in _RANGES:
|
||||
if lo <= number <= hi:
|
||||
return bg, fg
|
||||
return _FALLBACK
|
||||
|
||||
|
||||
class BallsBar(tk.Canvas):
|
||||
"""
|
||||
Horizontal row of numbered lottery balls drawn on a tk.Canvas.
|
||||
|
||||
numbers — list of main ball integers
|
||||
bonus — optional single bonus ball integer (drawn after a gap, in red)
|
||||
radius — ball radius in pixels (default 14 → 28 px diameter)
|
||||
highlights — {number: (bg_hex, fg_hex)} overrides for specific balls
|
||||
(e.g. green for matched, grey for unmatched in ticket checker)
|
||||
"""
|
||||
|
||||
_GAP = 3 # px between adjacent balls
|
||||
_SEP = 10 # extra px gap before the bonus ball
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
numbers: list[int],
|
||||
bonus: int | None = None,
|
||||
radius: int = 14,
|
||||
highlights: dict | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self._radius = radius
|
||||
self._highlights = highlights or {}
|
||||
|
||||
diam = radius * 2
|
||||
n_main = len(numbers)
|
||||
n_bonus = 1 if bonus is not None else 0
|
||||
|
||||
width = (
|
||||
n_main * (diam + self._GAP)
|
||||
+ (self._SEP + n_bonus * (diam + self._GAP) if n_bonus else 0)
|
||||
+ 4
|
||||
)
|
||||
height = diam + 4
|
||||
|
||||
# Inherit parent background so the canvas blends in seamlessly
|
||||
if "bg" not in kwargs and "background" not in kwargs:
|
||||
try:
|
||||
kwargs["background"] = parent.cget("background")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
kwargs.setdefault("highlightthickness", 0)
|
||||
kwargs.setdefault("bd", 0)
|
||||
super().__init__(parent, width=width, height=height, **kwargs)
|
||||
|
||||
self._draw(numbers, bonus, diam)
|
||||
|
||||
def _draw(self, numbers: list[int], bonus: int | None, diam: int):
|
||||
r = self._radius
|
||||
x = 2 + r
|
||||
y = r + 2
|
||||
|
||||
for num in numbers:
|
||||
bg, fg = self._highlights.get(num) or ball_color(num, False)
|
||||
self._ball(x, y, num, bg, fg)
|
||||
x += diam + self._GAP
|
||||
|
||||
if bonus is not None:
|
||||
x += self._SEP
|
||||
bg, fg = self._highlights.get(bonus) or ball_color(bonus, True)
|
||||
self._ball(x, y, bonus, bg, fg)
|
||||
|
||||
def _ball(self, cx: int, cy: int, number: int, bg: str, fg: str):
|
||||
r = self._radius - 1
|
||||
self.create_oval(cx - r, cy - r, cx + r, cy + r,
|
||||
fill=bg, outline="#aaaaaa", width=1)
|
||||
size = 7 if number >= 10 else 8
|
||||
self.create_text(cx, cy, text=str(number),
|
||||
font=("TkDefaultFont", size, "bold"), fill=fg)
|
||||
Reference in New Issue
Block a user