419 lines
No EOL
16 KiB
Python
Executable file
419 lines
No EOL
16 KiB
Python
Executable file
#!/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
|
||
names = [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
|
||
)
|
||
|
||
for i, table in enumerate(TABLES):
|
||
nbCol = 4
|
||
col = i % nbCol
|
||
row = len(PLAYERS) + 4 + (i // nbCol) + int(error_add_player.cget("text") != "")
|
||
|
||
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(frm, text=f"Table {table.id}", padding=12, style="Table.TLabelframe")
|
||
tableFrame.grid(column=col, row=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
|
||
names = [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() |