I developed a Python application designed to streamline the process of organizing teams and matchups for game nights, which frequently involve more than 10 players and up to 10 different games. This tool simplifies the coordination and enhances the overall experience by automating team formation and matchup generation.
import random
import csv
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from tkinter import messagebox
from tkinter.ttk import Treeview
def generate_teams(players):
random.shuffle(players)
return [(players[i], players[i + 1]) for i in range(0, len(players), 2)]
def generate_1v1_matchups(players, num_games, game_names, teammates):
if len(players) % 2 != 0:
raise ValueError("Number of players must be even.")
num_players = len(players)
matchups = []
prior_matchups = set()
for game_index in range(num_games):
found_valid = False
attempts = 0
max_attempts = 1000
while not found_valid and attempts < max_attempts:
random.shuffle(players)
round_matchups = []
used = set()
valid = True
for i in range(0, num_players, 2):
p1, p2 = players[i], players[i + 1]
pair = frozenset((p1, p2))
# Check if they've already played or are teammates
if pair in prior_matchups or pair in teammates or p1 in used or p2 in used:
valid = False
break
round_matchups.append((game_names[game_index], p1, p2))
used.add(p1)
used.add(p2)
if valid:
for _, p1, p2 in round_matchups:
prior_matchups.add(frozenset((p1, p2)))
matchups.append(round_matchups)
found_valid = True
else:
attempts += 1
if not found_valid:
matchups.append([("Invalid Matchup", "Too many constraints", "Try again", "")])
return matchups
def start_gui():
def generate_results():
try:
num_players = int(player_count_entry.get())
num_games = int(game_count_entry.get())
if num_players % 2 != 0 or num_players <= 0:
messagebox.showerror("Error", "Number of players must be a positive even number.")
return
if num_games <= 0:
messagebox.showerror("Error", "Number of games must be a positive number.")
return
players = player_names_entry.get().split(",") # Get player names from the text box, separated by commas
players = [player.strip() for player in players] # Strip extra spaces
if len(players) != num_players:
messagebox.showerror("Error", f"Number of player names provided does not match the number of players ({num_players}).")
return
game_names = game_names_entry.get().split(",") # Get game names from the text box, separated by commas
game_names = [game.strip() for game in game_names] # Strip extra spaces
if len(game_names) != num_games:
messagebox.showerror("Error", f"Number of game names provided does not match the number of games ({num_games}).")
return
team_tree.delete(*team_tree.get_children())
tree.delete(*tree.get_children())
teams = generate_teams(players[:])
for player1, player2 in teams:
team_tree.insert("", "end", values=(player1, player2))
teammates = set(frozenset((p1, p2)) for p1, p2 in teams)
matchups = generate_1v1_matchups(players[:], num_games, game_names, teammates)
for game in matchups:
for game_name, player1, player2, *rest in game:
winner = rest[0] if rest else ""
item_id = tree.insert("", "end", values=(game_name, player1, player2, winner))
if game_name == "Invalid Matchup":
tree.item(item_id, tags=("invalid",))
tree.tag_configure("invalid", background="#ffd6d6")
except ValueError:
messagebox.showerror("Error", "Invalid input. Please enter valid numbers.")
def reset_table():
tree.delete(*tree.get_children())
team_tree.delete(*team_tree.get_children())
def export_results():
with open("matchups_results.csv", "w", newline="") as file:
writer = csv.writer(file)
writer.writerow(["Game Name", "Player 1", "Player 2", "Winner"])
for row in tree.get_children():
writer.writerow(tree.item(row)['values'])
messagebox.showinfo("Export", "Results exported to matchups_results.csv")
def select_winner(item_id, winner_name):
values = list(tree.item(item_id, "values"))
if values[3]: # Already has a winner
return
values[3] = winner_name
tree.item(item_id, values=values)
tree.item(item_id, tags=("winner",))
tree.tag_configure("winner", background="#d1ffd1")
def on_tree_select(event):
selected_item = tree.selection()
if selected_item:
item_id = selected_item[0]
values = tree.item(item_id, "values")
if values[3] or values[0] == "Invalid Matchup":
return
winner_window = ttk.Toplevel(root)
winner_window.title("Select Winner")
winner_window.geometry("250x100")
ttk.Label(winner_window, text=f"Select winner for {values[1]} vs {values[2]}:").pack(pady=5)
ttk.Button(winner_window, text=f"{values[1]} Wins",
command=lambda: [select_winner(item_id, values[1]), winner_window.destroy()]).pack(side=LEFT, padx=10)
ttk.Button(winner_window, text=f"{values[2]} Wins",
command=lambda: [select_winner(item_id, values[2]), winner_window.destroy()]).pack(side=RIGHT, padx=10)
def on_focus_in(event):
event.widget.select_range(0, len(event.widget.get()))
root = ttk.Window(themename="darkly")
root.title("Team and Matchup Generator")
root.geometry("850x600")
style = ttk.Style():
style.configure(".", font=("Segoe UI", 12)) # Windows-like clean font
# Adjust Treeview row height
style.configure("Treeview", rowheight=30)
input_frame = ttk.Frame(root, padding=10)
input_frame.pack(pady=10)
team_frame = ttk.Frame(root, padding=10)
team_frame.pack(pady=5, fill=X)
ttk.Label(team_frame, text="Teams:").pack()
team_tree = Treeview(team_frame, columns=("Player 1", "Player 2"), show="headings", height=5)
team_tree.pack(fill=X)
for col in ["Player 1", "Player 2"]:
team_tree.heading(col, text=col)
team_tree.column(col, width=260, anchor="center", minwidth=150, stretch=True)
ttk.Label(input_frame, text="Number of players (even):").grid(row=0, column=0, padx=5, pady=5)
player_count_entry = ttk.Entry(input_frame)
player_count_entry.grid(row=0, column=1, padx=5, pady=5)
player_count_entry.bind("", on_focus_in) # Auto-select text on focus
ttk.Label(input_frame, text="Number of games:").grid(row=1, column=0, padx=5, pady=5)
game_count_entry = ttk.Entry(input_frame)
game_count_entry.grid(row=1, column=1, padx=5, pady=5)
game_count_entry.bind("", on_focus_in) # Auto-select text on focus
ttk.Label(input_frame, text="Enter player names (comma separated):").grid(row=2, column=0, padx=5, pady=5)
player_names_entry = ttk.Entry(input_frame)
player_names_entry.grid(row=2, column=1, padx=5, pady=5)
ttk.Label(input_frame, text="Enter game names (comma separated):").grid(row=3, column=0, padx=5, pady=5)
game_names_entry = ttk.Entry(input_frame)
game_names_entry.grid(row=3, column=1, padx=5, pady=5)
ttk.Button(input_frame, text="🎲 Generate", command=generate_results).grid(row=4, column=0, pady=10)
ttk.Button(input_frame, text="🔁 Reset", command=reset_table).grid(row=4, column=1, pady=10)
ttk.Button(input_frame, text="💾 Export", command=export_results).grid(row=4, column=2, pady=10)
tree_frame = ttk.Frame(root, padding=10)
tree_frame.pack(pady=10, fill=BOTH, expand=True)
tree = Treeview(tree_frame, columns=("Game Name", "Player 1", "Player 2", "Winner"), show="headings")
tree.pack(side=LEFT, fill=BOTH, expand=True)
for col in ["Game Name", "Player 1", "Player 2", "Winner"]:
tree.heading(col, text=col)
tree.column(col, width=260, anchor="center", minwidth=150, stretch=True)
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview)
scrollbar.pack(side=RIGHT, fill="y")
tree.configure(yscrollcommand=scrollbar.set)
tree.bind("<>", on_tree_select)
root.mainloop()
if __name__ == "__main__":
start_gui()