#!/usr/bin/env python3 from tkinter import * from tkinter import ttk from math import inf from player import Player from table import Table PLAYERS: list[Player] = [] TABLES: list[Table] = [] ID_TABLES = 1 INITIAL_SCORE = 30000 UMA1 = 15000 UMA2 = 5000 UMA3 = -5000 UMA4 = -15000 # init tkinter root = Tk() root.title("🀄 RonRunner - Mahjong Tournament Scoring 🀄") root.geometry("1000x700") # Color scheme (Modern dark theme) BG_PRIMARY = "#1e1e2e" BG_SECONDARY = "#2a2a3e" BG_TERTIARY = "#3a3a4e" FG_PRIMARY = "#ffffff" FG_SECONDARY = "#a0a0b0" ACCENT_PRIMARY = "#6366f1" ACCENT_SECONDARY = "#8b5cf6" SUCCESS_COLOR = "#10b981" ERROR_COLOR = "#ef4444" WARNING_COLOR = "#f97316" # configure style style = ttk.Style() style.theme_use('clam') root.configure(bg=BG_PRIMARY) style.configure("TFrame", background=BG_PRIMARY, foreground=FG_PRIMARY) style.configure("TLabel", background=BG_PRIMARY, foreground=FG_PRIMARY, font=("Segoe UI", 10)) style.configure("Title.TLabel", background=BG_PRIMARY, foreground=FG_PRIMARY, font=("Segoe UI", 16, "bold")) style.configure("Header.TLabel", background=BG_PRIMARY, foreground=FG_PRIMARY, font=("Segoe UI", 12, "bold")) style.configure("TButton", font=("Segoe UI", 10, "bold"), padding=8) style.map("TButton", background=[("active", ACCENT_SECONDARY), ("pressed", "#4f46e5")]) style.configure("TEntry", fieldbackground=BG_TERTIARY, foreground=FG_PRIMARY, font=("Segoe UI", 10), padding=6) style.configure("TCombobox", fieldbackground=BG_TERTIARY, foreground=FG_PRIMARY, font=("Segoe UI", 10), arrowcolor=ACCENT_PRIMARY) style.map("TCombobox", fieldbackground=[("readonly", BG_TERTIARY)], foreground=[("readonly", FG_PRIMARY)]) style.configure("Placeholder.TCombobox", foreground=FG_SECONDARY) # Function to style popups def style_popup(popup): popup.configure(bg=BG_PRIMARY) for widget in popup.winfo_children(): if isinstance(widget, (Label, Entry)): widget.configure(bg=BG_PRIMARY) # configure scrollbar canvas = Canvas(root, bg=BG_PRIMARY, highlightthickness=0) scrollbar = ttk.Scrollbar(root, orient='vertical', command=canvas.yview) scrollbarFrame = Frame(canvas, bg=BG_PRIMARY) scrollbarFrame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollbarFrame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.grid(column=0, row=0, sticky="nsew") scrollbar.grid(column=1, row=0, sticky="ns") root.grid_rowconfigure(0, weight=1) root.grid_columnconfigure(0, weight=1) frm = scrollbarFrame # activate scrollbar with pad/mouse def _on_mousewheel(event): canvas.yview_scroll(int(-1*(event.delta/120)), "units") def _on_scroll_up(event): canvas.yview_scroll(-1, "units") def _on_scroll_down(event): canvas.yview_scroll(1, "units") def _bind_mousewheel(widget): widget.bind("", _on_mousewheel) widget.bind("", _on_scroll_up) widget.bind("", _on_scroll_down) for child in widget.winfo_children(): _bind_mousewheel(child) _bind_mousewheel(frm) # init ui # players ttk.Label(frm, text="Rank", font=("Segoe UI", 12, "bold"), foreground=ACCENT_PRIMARY).grid(column=0, row=0, padx=15, pady=15) ttk.Label(frm, text="Player", font=("Segoe UI", 12, "bold"), foreground=ACCENT_PRIMARY).grid(column=1, row=0, padx=15, pady=15) ttk.Label(frm, text="Score", font=("Segoe UI", 12, "bold"), foreground=ACCENT_PRIMARY).grid(column=2, row=0, padx=15, pady=15) entry_player_name = ttk.Entry(frm, width=30) entry_player_name.grid(column=1, row=len(PLAYERS) + 1, padx=15, pady=12) error_add_player = ttk.Label(frm, text="", font=('Segoe UI', 9), foreground=ERROR_COLOR) error_add_player.grid(column=1, row=len(PLAYERS) + 2, padx=15, pady=10) # adding player def addPlayer(entry: Entry): txt = entry.get() if not txt: error_add_player.config(text="No name inserted") elif txt in [p.name for p in PLAYERS]: error_add_player.config(text=f"Player {txt} already exists") else: error_add_player.config(text="") PLAYERS.append(Player(txt)) entry.delete(0, END) refreshUi() btn_player_name = ttk.Button(frm, text="➕ Add", command=lambda : addPlayer(entry_player_name)) btn_player_name.grid(column=2, row=len(PLAYERS) + 1, padx=15, pady=12) # tables # create table def addTable(): popup = Toplevel(root) popup.title("Create a new table") popup.geometry("350x280") style_popup(popup) p1_var = StringVar(value="Select player 1") p2_var = StringVar(value="Select player 2") p3_var = StringVar(value="Select player 3") p4_var = StringVar(value="Select player 4") # names list sorted alphabetically names = sorted([p.name for p in PLAYERS]) ttk.Label(popup, text="Player 1", font=("Segoe UI", 10)).grid(column=0, row=0, padx=12, pady=12) combo1 = ttk.Combobox(popup, textvariable=p1_var, values=names, state="readonly", width=25) combo1.grid(column=1, row=0, padx=12, pady=8) ttk.Label(popup, text="Player 2", font=("Segoe UI", 10)).grid(column=0, row=1, padx=12, pady=12) combo2 = ttk.Combobox(popup, textvariable=p2_var, values=names, state="readonly", width=25) combo2.grid(column=1, row=1, padx=12, pady=8) ttk.Label(popup, text="Player 3", font=("Segoe UI", 10)).grid(column=0, row=2, padx=12, pady=12) combo3 = ttk.Combobox(popup, textvariable=p3_var, values=names, state="readonly", width=25) combo3.grid(column=1, row=2, padx=12, pady=8) ttk.Label(popup, text="Player 4", font=("Segoe UI", 10)).grid(column=0, row=3, padx=12, pady=12) combo4 = ttk.Combobox(popup, textvariable=p4_var, values=names, state="readonly", width=25) combo4.grid(column=1, row=3, padx=12, pady=8) def createTable(): p1_name = p1_var.get() p2_name = p2_var.get() p3_name = p3_var.get() p4_name = p4_var.get() if p1_name != "Select player 1" and p2_name != "Select player 2" and p3_name != "Select player 3" and p4_name != "Select player 4": p1 = next((p for p in PLAYERS if p.name == p1_name), None) p2 = next((p for p in PLAYERS if p.name == p2_name), None) p3 = next((p for p in PLAYERS if p.name == p3_name), None) p4 = next((p for p in PLAYERS if p.name == p4_name), None) if len(set([p1, p2, p3, p4])) == 4: global ID_TABLES TABLES.append(Table(ID_TABLES, p1, p2, p3, p4, INITIAL_SCORE)) ID_TABLES += 1 cancelTopLevel(popup) ttk.Button(popup, text="✓ Create", command=createTable).grid(column=1, row=4, padx=12, pady=20) ttk.Button(popup, text="✕ Cancel", command=lambda : cancelTopLevel(popup)).grid(column=2, row=4, padx=12, pady=20) btn_create_table = ttk.Button(frm, text="➕ Create table", command=addTable) btn_create_table.grid( column=1, row=len(PLAYERS) + 2, padx=15, pady=20 ) # refresh ui def refreshUi(): # update scores updateScore() # remove old labels for widget in frm.winfo_children(): if widget not in [ entry_player_name, btn_player_name, btn_create_table, error_add_player ]: info = widget.grid_info() if info.get('row', 0) > 0 and info.get('row', 0) < 100: widget.destroy() # show players PLAYERS.sort(key=lambda p : p.score, reverse=True) for i in range(len(PLAYERS)): ttk.Label(frm, text=f"{i+1}", font=("Segoe UI", 11, "bold"), foreground=ACCENT_PRIMARY).grid(column=0, row=i+1, padx=15, pady=12) playerLabel = ttk.Label(frm, text=PLAYERS[i].name, font=("Segoe UI", 10), foreground=FG_PRIMARY) playerLabel.grid(column=1, row=i+1, padx=15, pady=12) ttk.Label(frm, text=f"{PLAYERS[i].score}", font=("Segoe UI", 11, "bold"), foreground=SUCCESS_COLOR).grid(column=2, row=i+1, padx=15, pady=12) # change player name def onClick(event, player=PLAYERS[i]): popup = Toplevel(root) popup.title(f"Edit - {player.name}") popup.geometry("340x170") style_popup(popup) entry = ttk.Entry(popup, width=30) entry.insert(0, player.name) entry.grid(column=0, row=0, columnspan=4, padx=12, pady=12) error_change_player = ttk.Label(popup, text="", font=('Segoe UI', 9), foreground=ERROR_COLOR) error_change_player.grid(column=0, row=1, columnspan=4, padx=12, pady=10) def changeName(): name = entry.get() if not name: error_change_player.config(text="No name inserted") elif name in [p.name for p in PLAYERS]: error_change_player.config(text=f"Player {name} already exists") else: error_change_player.config(text="") player.name = name entry.delete(0, END) cancelTopLevel(popup) def deletePlayer(): for t in TABLES: if player.name in [t.p1.name, t.p2.name, t.p3.name, t.p4.name]: error_change_player.config(text=f"Cannot delete: player still in table {t.id}") return PLAYERS.remove(player) cancelTopLevel(popup) ttk.Button(popup, text="✓ Change", command=changeName).grid(column=0, row=2, padx=6, pady=10) ttk.Button(popup, text="✕ Cancel", command=lambda : cancelTopLevel(popup)).grid(column=1, row=2, padx=6, pady=10) ttk.Button(popup, text="⊘ Delete", command=deletePlayer).grid(column=2, row=2, padx=6, pady=10) playerLabel.bind("", onClick) # adding player entry_player_name.grid(column=1, row=len(PLAYERS) + 1) btn_player_name.grid(column=2, row=len(PLAYERS) + 1) error_add_player.grid(column=1, row=len(PLAYERS) + 2, padx=15, pady=10) # show tables ttk.Label( frm, text="Tables", font=("Segoe UI", 14, "bold"), foreground=ACCENT_PRIMARY ).grid( column=0, row=len(PLAYERS) + 3 + int(error_add_player.cget("text") != ""), columnspan=3, padx=15, pady=25 ) tables_row = len(PLAYERS) + 4 + int(error_add_player.cget("text") != "") tables_frame = Frame(frm, bg=BG_PRIMARY) tables_frame.grid(column=0, row=tables_row, columnspan=3, sticky="ew", padx=15, pady=12) for i, table in enumerate(TABLES): nbCol = 4 col = i % nbCol table_row = i // nbCol style = ttk.Style() style.theme_use('clam') if (table.s1 + table.s2 + table.s3 + table.s4 != 4 * INITIAL_SCORE): colour = ERROR_COLOR elif ([table.s1, table.s2, table.s3, table.s4] == [INITIAL_SCORE] * 4): colour = WARNING_COLOR else: colour = SUCCESS_COLOR style.configure("Table.TLabelframe", background=BG_PRIMARY, foreground=FG_PRIMARY, borderwidth=2, relief="solid") style.configure("Table.TLabelframe.Label", background=BG_PRIMARY, foreground=FG_PRIMARY) tableFrame = ttk.LabelFrame(tables_frame, text=f"Table {table.id}", padding=12, style="Table.TLabelframe") tableFrame.grid(column=col, row=table_row, padx=12, pady=12, sticky="ew") Label(tableFrame, text=f"{table.p1.name} : {table.s1}", font=("Segoe UI", 9), foreground=FG_PRIMARY, bg=BG_PRIMARY).pack(anchor="w") Label(tableFrame, text=f"{table.p2.name} : {table.s2}", font=("Segoe UI", 9), foreground=FG_PRIMARY, bg=BG_PRIMARY).pack(anchor="w") Label(tableFrame, text=f"{table.p3.name} : {table.s3}", font=("Segoe UI", 9), foreground=FG_PRIMARY, bg=BG_PRIMARY).pack(anchor="w") Label(tableFrame, text=f"{table.p4.name} : {table.s4}", font=("Segoe UI", 9), foreground=FG_PRIMARY, bg=BG_PRIMARY).pack(anchor="w") def onClick(event, table=table): popup = Toplevel(root) popup.title(f"Edit table") popup.geometry("500x280") style_popup(popup) p1_var = StringVar(value=table.p1.name) p2_var = StringVar(value=table.p2.name) p3_var = StringVar(value=table.p3.name) p4_var = StringVar(value=table.p4.name) # names list sorted alphabetically names = sorted([p.name for p in PLAYERS]) ttk.Label(popup, text="Player 1", font=("Segoe UI", 10)).grid(column=0, row=0, padx=10, pady=10) combo1 = ttk.Combobox(popup, textvariable=p1_var, values=names, state="readonly", width=20) combo1.grid(column=1, row=0, padx=10, pady=8) entry1 = ttk.Entry(popup, width=12) entry1.insert(0, table.s1) entry1.grid(column=2, row=0, padx=10, pady=10) ttk.Label(popup, text="Player 2", font=("Segoe UI", 10)).grid(column=0, row=1, padx=10, pady=10) combo2 = ttk.Combobox(popup, textvariable=p2_var, values=names, state="readonly", width=20) combo2.grid(column=1, row=1, padx=10, pady=8) entry2 = ttk.Entry(popup, width=12) entry2.insert(0, table.s2) entry2.grid(column=2, row=1, padx=10, pady=10) ttk.Label(popup, text="Player 3", font=("Segoe UI", 10)).grid(column=0, row=2, padx=10, pady=10) combo3 = ttk.Combobox(popup, textvariable=p3_var, values=names, state="readonly", width=20) combo3.grid(column=1, row=2, padx=10, pady=8) entry3 = ttk.Entry(popup, width=12) entry3.insert(0, table.s3) entry3.grid(column=2, row=2, padx=10, pady=10) ttk.Label(popup, text="Player 4", font=("Segoe UI", 10)).grid(column=0, row=3, padx=10, pady=10) combo4 = ttk.Combobox(popup, textvariable=p4_var, values=names, state="readonly", width=20) combo4.grid(column=1, row=3, padx=10, pady=8) entry4 = ttk.Entry(popup, width=12) entry4.insert(0, table.s4) entry4.grid(column=2, row=3, padx=10, pady=10) def changeTable(): p1_name = p1_var.get() p2_name = p2_var.get() p3_name = p3_var.get() p4_name = p4_var.get() try: s1 = int(entry1.get()) s2 = int(entry2.get()) s3 = int(entry3.get()) s4 = int(entry4.get()) except: return if p1_name != "Select player 1" and p2_name != "Select player 2" and p3_name != "Select player 3" and p4_name != "Select player 4": p1 = next((p for p in PLAYERS if p.name == p1_name), None) p2 = next((p for p in PLAYERS if p.name == p2_name), None) p3 = next((p for p in PLAYERS if p.name == p3_name), None) p4 = next((p for p in PLAYERS if p.name == p4_name), None) if len(set([p1, p2, p3, p4])) == 4: table.p1 = p1 table.p2 = p2 table.p3 = p3 table.p4 = p4 table.s1 = s1 table.s2 = s2 table.s3 = s3 table.s4 = s4 cancelTopLevel(popup) def deleteTable(): TABLES.remove(table) cancelTopLevel(popup) ttk.Button(popup, text="✓ Ok", command=changeTable).grid(column=1, row=4, padx=10, pady=20) ttk.Button(popup, text="✕ Cancel", command=lambda : cancelTopLevel(popup)).grid(column=2, row=4, padx=10, pady=20) ttk.Button(popup, text="⊘ Delete", command=deleteTable).grid(column=3, row=4, padx=10, pady=20) tableFrame.bind("", onClick) for child in tableFrame.winfo_children(): child.bind("", onClick) # adding table btn_create_table.grid( column=1, row=len(PLAYERS) + 2 + int(error_add_player.cget("text") != ""), padx=15, pady=20 ) def cancelTopLevel(tl: Toplevel): tl.destroy() refreshUi() def updateScore(): for p in PLAYERS: p.score = -inf for t in TABLES: for pp in [t.p1, t.p2, t.p3, t.p4]: if pp.score == -inf: pp.score = 0 scores = [ (t.p1, t.s1), (t.p2, t.s2), (t.p3, t.s3), (t.p4, t.s4) ] scores.sort(key=lambda c : c[1], reverse=True) scores[0][0].score += scores[0][1] + UMA1 - INITIAL_SCORE scores[1][0].score += scores[1][1] + UMA2 - INITIAL_SCORE scores[2][0].score += scores[2][1] + UMA3 - INITIAL_SCORE scores[3][0].score += scores[3][1] + UMA4 - INITIAL_SCORE refreshUi() root.mainloop()