Tyler Long | Code Portfolio

Game Night Application

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()