RonRunner/ronrunner_1.0~beta1-1/usr/bin/ronrunner
2026-02-10 18:18:54 +01:00

423 lines
No EOL
17 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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(
"<Configure>",
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("<MouseWheel>", _on_mousewheel)
widget.bind("<Button-4>", _on_scroll_up)
widget.bind("<Button-5>", _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("<Button-1>", 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("<Button-1>", onClick)
for child in tableFrame.winfo_children():
child.bind("<Button-1>", 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()